diff --git a/echoprometheus/prometheus.go b/echoprometheus/prometheus.go index 89ae77e..1bb9a5a 100644 --- a/echoprometheus/prometheus.go +++ b/echoprometheus/prometheus.go @@ -249,7 +249,15 @@ func (conf MiddlewareConfig) ToMiddleware() (echo.MiddlewareFunc, error) { if url == "" { // as of Echo v4.10.1 path is empty for 404 cases (when router did not find any matching routes) // in this case we use actual path from request to have some distinction in Prometheus - url = c.Request().URL.Path + + // Referencing Go documentation (https://cs.opensource.google/go/go/+/refs/tags/go1.21.3:src/net/url/url.go;l=357-359): + // We first check the RawPath, which is the original, escaped form. It's important to check RawPath first because + // it preserves the original encoding of the URL. Using Path (decoded form) can sometimes result invalid UTF-8 characters. + if c.Request().URL.RawPath != "" { + url = c.Request().URL.RawPath + } else { + url = c.Request().URL.Path + } } status := c.Response().Status diff --git a/echoprometheus/prometheus_test.go b/echoprometheus/prometheus_test.go index e4397b4..ad0e1ea 100644 --- a/echoprometheus/prometheus_test.go +++ b/echoprometheus/prometheus_test.go @@ -330,3 +330,22 @@ func unregisterDefaults(subsystem string) { Help: "The HTTP request sizes in bytes.", }) } + +func TestToMiddleware_RawPath(t *testing.T) { + middleware, err := MiddlewareConfig{}.ToMiddleware() + assert.NoError(t, err) + + next := echo.HandlerFunc(func(c echo.Context) error { + return nil + }) + + handler := middleware(next) + + e := echo.New() + + req := httptest.NewRequest(http.MethodGet, "/v1/%a1", http.NoBody) + rec := httptest.NewRecorder() + echoContext := e.NewContext(req, rec) + + assert.NoError(t, handler(echoContext)) +}