Repository: TwiN/gatus

Issue:
Endpoint page just shows wait animation
### Describe the bug

I've added a couple of endpoints which have never been green (service just being created).
For one of them the details page just show the wait animation. And I cannot find entry in the log corresponding to that behavior.

### What do you see?

<img width="651" height="346" alt="Image" src="https://github.com/user-attachments/assets/00251c58-e703-44ef-8232-50e026549d4a" />

### What do you expect to see?

Regular endpoint page reflecting something like

<img width="275" height="185" alt="Image" src="https://github.com/user-attachments/assets/7c4e214d-0a8e-416e-9a14-aefec4934a39" />

### List the steps that must be taken to reproduce this issue

_No response_

### Version

_No response_

### Additional information

_No response_

Hints from issue comments:
@TwiN: Can you share the configuration of that buggy endpoint? You're really not giving me a lot to look at 😅@amai2012: Here's a config (manually reduced from the complete config, not verified in real life, and some names changed...):
~~~ yaml
---
metrics: true

alerting:
  teams-workflows:
    webhook-url: "${WEBHOOK_MSTEAMS}"
    title: "&#x26D1; our-gatus-${ENV_NAME}"
    default-alert:
      description: "health check failed"
      failure-threshold: 2
      send-on-resolved: true
      success-threshold: 1

storage:
  type: sqlite
  path: /data/data.db

default-endpoint_https_secure: &defaults_https_secure
  interval: 5m
  client:
    insecure: false
    timeout: 500ms
  conditions:
    - "[STATUS] == 200"
    - "[CERTIFICATE_EXPIRATION] > 72h"

endpoints:
  - name: "Prometheus (Services+Federation)"
    url: "https://prometheus.some.where/-/healthy"
    group: "XXX Services (YYY)"
    <<: *defaults_https_secure
    conditions:
      - "[STATUS] == 200"
      - "[CERTIFICATE_EXPIRATION] > 72h"
      - "[BODY] == pat(Prometheus Server is Healthy.)"
~~~
@TwiN: Have you tried removing the + and ( ) from the name and group?@amai2012: I've added a copy of the given endpoint with a difference name "Prometheus Services and Federation" - and the details page for that is working fine!@TwiN: It's likely because of the special characters you had in the name/group@amai2012: @TwiN  I can accept that the code isn't fixed/changed, but I'd ask for a doc fix at least:
https://github.com/TwiN/gatus/blob/master/README.md should mention which characters I am allowed to use or which I should avoid.@TwiN: Adding support for `+`, `(` and `)` isn't a bad idea actually. 

Give me a couple minutes, I'll create a PR to support those characters, and you can be the first to test that it works.

Solution patch:
diff --git a/README.md b/README.md
index ca0c84e0..489a6cf3 100644
--- a/README.md
+++ b/README.md
@@ -366,7 +366,7 @@ or send an HTTP request:
 POST /api/v1/endpoints/{key}/external?success={success}&error={error}&duration={duration}
 ```
 Where:
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
   - Using the example configuration above, the key would be `core_ext-ep-test`.
 - `{success}` is a boolean (`true` or `false`) value indicating whether the health check was successful or not.
 - `{error}` (optional): a string describing the reason for a failed health check. If {success} is false, this should contain the error message; if the check is successful.
@@ -3294,7 +3294,7 @@ The path to generate a badge is the following:
 ```
 Where:
 - `{duration}` is `30d`, `7d`, `24h` or `1h`
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 For instance, if you want the uptime during the last 24 hours from the endpoint `frontend` in the group `core`,
 the URL would look like this:
@@ -3320,7 +3320,7 @@ The path to generate a badge is the following:
 /api/v1/endpoints/{key}/health/badge.svg
 ```
 Where:
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 For instance, if you want the current status of the endpoint `frontend` in the group `core`,
 the URL would look like this:
@@ -3337,7 +3337,7 @@ The path to generate a badge is the following:
 /api/v1/endpoints/{key}/health/badge.shields
 ```
 Where:
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 For instance, if you want the current status of the endpoint `frontend` in the group `core`,
 the URL would look like this:
@@ -3360,7 +3360,7 @@ The endpoint to generate a badge is the following:
 ```
 Where:
 - `{duration}` is `30d`, `7d`, `24h` or `1h`
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 #### Response time (chart)
 ![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/chart.svg)
@@ -3373,7 +3373,7 @@ The endpoint to generate a response time chart is the following:
 ```
 Where:
 - `{duration}` is `30d`, `7d`, or `24h`
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 ##### How to change the color thresholds of the response time badge
 To change the response time badges' threshold, a corresponding configuration can be added to an endpoint.
@@ -3431,7 +3431,7 @@ The path to get raw uptime data for an endpoint is:
 ```
 Where:
 - `{duration}` is `30d`, `7d`, `24h` or `1h`
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 For instance, if you want the raw uptime data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
 ```
@@ -3445,7 +3445,7 @@ The path to get raw response time data for an endpoint is:
 ```
 Where:
 - `{duration}` is `30d`, `7d`, `24h` or `1h`
-- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.` and `#` replaced by `-`.
+- `{key}` has the pattern `<GROUP_NAME>_<ENDPOINT_NAME>` in which both variables have ` `, `/`, `_`, `,`, `.`, `#`, `(`, `)`, `+` and `&` replaced by `-`.
 
 For instance, if you want the raw response time data for the last 24 hours from the endpoint `frontend` in the group `core`, the URL would look like this:
 ```
diff --git a/config/key/key.go b/config/key/key.go
index d0b931dc..f06aa1fb 100644
--- a/config/key/key.go
+++ b/config/key/key.go
@@ -15,5 +15,9 @@ func sanitize(s string) string {
 	s = strings.ReplaceAll(s, ",", "-")
 	s = strings.ReplaceAll(s, " ", "-")
 	s = strings.ReplaceAll(s, "#", "-")
+	s = strings.ReplaceAll(s, "(", "-")
+	s = strings.ReplaceAll(s, ")", "-")
+	s = strings.ReplaceAll(s, "+", "-")
+	s = strings.ReplaceAll(s, "&", "-")
 	return s
-}
\ No newline at end of file
+}



Test patch:
diff --git a/config/key/key_test.go b/config/key/key_test.go
index 34179cfc..3c082c3f 100644
--- a/config/key/key_test.go
+++ b/config/key/key_test.go
@@ -29,6 +29,21 @@ func TestConvertGroupAndNameToKey(t *testing.T) {
 			Name:           "name",
 			ExpectedOutput: "_name",
 		},
+		{
+			GroupName:      "API (v1)",
+			Name:           "endpoint",
+			ExpectedOutput: "api--v1-_endpoint",
+		},
+		{
+			GroupName:      "website (admin)",
+			Name:           "test",
+			ExpectedOutput: "website--admin-_test",
+		},
+		{
+			GroupName:      "search",
+			Name:           "query&filter",
+			ExpectedOutput: "search_query-filter",
+		},
 	}
 	for _, scenario := range scenarios {
 		t.Run(scenario.ExpectedOutput, func(t *testing.T) {