Skip to content

URL handling for escaped characters inconsistent between routes and requests #414

@mdales

Description

@mdales

Dream seems to handle URL encoding inconsistently between route registration and request handling, which caused me a bunch of confusing and debugging, and I wonder if it could be made more consistent, or am I just using things wrong?

Current Behaviour

When registering routes with Dream, the route patterns must contain unencoded (raw UTF-8) characters:

Dream.get "/photos/södermalm_pride/" handler  (* This works *)
Dream.get "/photos/s%C3%B6dermalm_pride/" handler  (* This does NOT match *)

However, when Dream logs incoming requests or when inspecting Dream.target, the paths are shown in their escaped form:

29.11.25 09:23:47.678    dream.logger  INFO REQ 1 GET /photos/s%C3%B6dermalm_pride/ 127.0.0.1:52990 fd 8 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.1 Safari/605.1.15

And the request here was from a link that I generated with the escaped for, so it came from:

              <a href="/photos/s%C3%B6dermalm_pride/">
                <img loading="lazy" src="/photos/s%C3%B6dermalm_pride/thumbnail.jpg" srcset="/photos/s%C3%B6dermalm_pride/thumbnail@2x.jpg 2x,
                    /photos/s%C3%B6dermalm_pride/thumbnail.jpg 1x" title="Södermalm pride" width="271" height="350" alt="Ett foto av en pride flagga över en väg på Södermalm på en solig dag.">
              </a>

It looks to me that Dream is unencoding the URL, despite the fact I'm being consistent in both the links my code generates being escaped and that's what I register for the route.

Expected Behaviour

I'd expect that I'd use the escaped form generally everywhere, given that this is what URLs require as I understand it.

I store URLs using Uri.t and render them with Uri.to_string, but now when I add routes I have to wrap them with Uri.pct_decode before I pass them to Dream routes.

Steps to repro

let () =
  Dream.run
  @@ Dream.logger
  @@ Dream.router [
    Dream.get "/photos/södermalm_pride/" (fun _ ->
      Dream.html "Unencoded route works!");
    Dream.get "/photos/s%C3%B6dermalm_pride/" (fun _ ->
      Dream.html "Encoded route works!");
    Dream.get "/**" (fun request ->
      Dream.log "Path received: %s" (Dream.target request);
      Dream.empty `Not_Found);
  ]

When requesting /photos/södermalm_pride/:

  • The first route (unencoded) matches (no matter which order I put the escaped and unescaped routes)
  • Logs show: INFO REQ 1 GET /photos/s%C3%B6dermalm_pride/ 127.0.0.1:53018

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions