go-chi / chi

lightweight, idiomatic and composable router for building Go HTTP services
https://go-chi.io
MIT License
18.26k stars 980 forks source link

Regex param doesn't match dots #758

Open Nyoroon opened 1 year ago

Nyoroon commented 1 year ago

Example program:

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"

    "github.com/go-chi/chi/v5"
)

func main() {
    mux := chi.NewMux()
    mux.Get("/{param:.+}.json", func(w http.ResponseWriter, r *http.Request) {
        param := chi.URLParam(r, "param")
        _, _ = fmt.Fprintf(w, "param=%s", param)
    })

    rec := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/param.json", nil)
    mux.ServeHTTP(rec, req)

    fmt.Printf("code: %d, body: %s\n", rec.Code, rec.Body)

    rec = httptest.NewRecorder()
    req = httptest.NewRequest(http.MethodGet, "/param.with.dots.json", nil)
    mux.ServeHTTP(rec, req)

    fmt.Printf("code: %d, body: %s\n", rec.Code, rec.Body)
}

Expected:

code: 200, body: param=param
code: 404, body: param=param.with.dots

Got:

code: 200, body: param=param
code: 404, body: 404 page not found
samsapti commented 1 year ago

@Nyoroon the . in regex means "any character", so if you want to match a literal ., you need to escape it. Your string would then be: "/{param:\\.+}.json".

Nyoroon commented 1 year ago

@Nyoroon the . in regex means "any character", so if you want to match a literal ., you need to escape it. Your string would then be: "/{param:\\.+}.json".

Yeah, and I want to match "any character", but it matches "any character except dot". I have an example in post.

samsapti commented 1 year ago

Okay, have you tried "/{param:[.\\.]+}.json"?

js-everts commented 1 year ago

@Nyoroon

This is not really a problem with the regex, it fails due to ambiguity.

If you were to modify the param to "/{param:.+}:json" (note the colon, it can be anything else) then make the request /param.with.dots:json, this will work because there is no ambiguity, chi knows everything before the colon (:) is supposed to be the param.

I suggest you define a the route like this r.Get("/{param:.+\\.json}", ...), then strip out and process the param manually in your handler.

6543 commented 1 year ago

811 only fix things partially ... see #813 but at lest it's a move in the right direction ...

VojtechVitek commented 6 days ago

@Nyoroon I agree that the following code is clearly buggy, as the route matches /param.json path but it doesn't match /param.tar.gz.

    r.Get("/{param:.+}.json", func(w http.ResponseWriter, r *http.Request) {
        param := chi.URLParam(r, "param")
        _, _ = fmt.Fprintf(w, "param=%s", param)
    })

However, fixing this bug is a very delicate work (see https://github.com/go-chi/chi/pull/813#pullrequestreview-2328041891) and potentially a breaking change and I'm not sure if it's worth it.

But.. I think I found a workaround for your example. Hope it helps!

Workaround

    r.Get("/{param:[^.]+}.{ext:.+}", func(w http.ResponseWriter, r *http.Request) {
        param := chi.URLParam(r, "param")
        ext := chi.URLParam(r, "ext")
        _, _ = fmt.Fprintf(w, "param=%s, ext=%s", param, ext)
    })

Example program

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"

    "github.com/go-chi/chi/v5"
)

func main() {
    mux := chi.NewMux()

    mux.Get("/{param:[^.]+}.{ext:.+}", func(w http.ResponseWriter, r *http.Request) {
        param := chi.URLParam(r, "param")
        ext := chi.URLParam(r, "ext")
        _, _ = fmt.Fprintf(w, "param=%s, ext=%s", param, ext)
    })

    rec := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/param.json", nil)
    mux.ServeHTTP(rec, req)

    fmt.Printf("code: %d, body: %s\n", rec.Code, rec.Body)

    rec = httptest.NewRecorder()
    req = httptest.NewRequest(http.MethodGet, "/param.tar.gz", nil)
    mux.ServeHTTP(rec, req)

    fmt.Printf("code: %d, body: %s\n", rec.Code, rec.Body)

    rec = httptest.NewRecorder()
    req = httptest.NewRequest(http.MethodGet, "/param.with.dots.json", nil)
    mux.ServeHTTP(rec, req)

    fmt.Printf("code: %d, body: %s\n", rec.Code, rec.Body)
}
$ go run ./
code: 200, body: param=param, ext=json
code: 200, body: param=param, ext=tar.gz
code: 200, body: param=param, ext=with.dots.json

Perhaps we could add this to the examples?