HttpRouter: ignoreDuplicateSlashes (on by default) collapses // inside query string values
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend
Research direction
Start at packages/effect/src/http/FindMyWay/internal/router.ts, in find() where removeDuplicateSlashes(path) is called before safeDecodeURI(path) (around L344-L350). Restructure so the querystring is split off first and only the path portion is normalized, preserving the invariant that sliceParameter works on the same string as noted in the comment above the call. Add a test under packages/effect/test/http covering a query value containing unencoded // (e.g. ?u=https://x.com/y) with default router config; done when the query value reaches the handler unchanged while duplicate slashes in the path are still collapsed.
Written by the indexing model from the issue text.
Description
What version of Effect is running?
[email protected]. The code is unchanged on main (757821fe).
What steps can reproduce the bug?
The vendored FindMyWay router enables ignoreDuplicateSlashes: true by default (router.ts#L56). In find(), when the URL is not "clean", removeDuplicateSlashes(path) runs on the whole URL (L344-L346) before safeDecodeURI(path) splits off the querystring (L350). As a result, // in the query string is collapsed too.
import { Effect } from "effect"
import { FindMyWay, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"
// 1. FindMyWay directly
const router = FindMyWay.make<string>({})
router.on("GET", "/a", "handler")
console.log("FindMyWay:", router.find("GET", "/a?u=https://x.com/y")?.searchParams)
// 2. HttpRouter end to end
const app = HttpRouter.add(
"GET",
"/oauth/authorize",
Effect.gen(function*() {
const params = yield* HttpServerRequest.ParsedSearchParams
return HttpServerResponse.jsonUnsafe(params)
})
)
for (const routerConfig of [undefined, { ignoreDuplicateSlashes: false }]) {
const { handler, dispose } = HttpRouter.toWebHandler(app, { routerConfig })
const res = await handler(new Request("http://localhost/oauth/authorize?resource=https://example.com/api/mcp"))
console.log(`HttpRouter (routerConfig: ${JSON.stringify(routerConfig)}):`, await res.text())
await dispose()
}
Output with bun repro.ts and [email protected]:
FindMyWay: { u: "https:/x.com/y" }
HttpRouter (routerConfig: undefined): {"resource":"https:/example.com/api/mcp"}
HttpRouter (routerConfig: {"ignoreDuplicateSlashes":false}): {"resource":"https://example.com/api/mcp"}
What is the expected behavior?
Duplicate-slash normalization should apply to the path only. Query values should reach the handler (ParsedSearchParams, HttpApi query schemas) unchanged: resource = "https://example.com/api/mcp".
What do you see instead?
resource = "https:/example.com/api/mcp". Percent-encoded values (https%3A%2F%2F…) are not affected. Unencoded values are, and RFC 3986 §3.4 allows unencoded : and / in a query. OAuth 2.0 parameters such as RFC 8707 resource and redirect_uri are URLs, and some clients send them unencoded. Because ignoreDuplicateSlashes is enabled by default, these requests are silently corrupted with no error. In our case, an MCP OAuth /authorize endpoint rejected a valid resource.
Additional information
Suggested fix: in find(), split on the first ? (or let safeDecodeURI separate the querystring first), run removeDuplicateSlashes on the path part only, then continue. The comment above that call says it must run before safeDecodeURI so sliceParameter works on the same string. Normalizing only the path part before reattaching ? + querystring keeps that invariant. Route registration (on, L98-L100) is not affected in practice because route paths have no query.
Upstream: delvedor/find-my-way has the same ordering on its current main (index.js find), and [email protected] with ignoreDuplicateSlashes: true produces the same { u: "https:/x.com/y" }. I found no upstream issue or fix for it. Upstream defaults this option to false, so the bug has more impact here, where it is enabled by default.
Workaround: disable the option through RouterConfig, for example HttpRouter.toWebHandler(app, { routerConfig: { ignoreDuplicateSlashes: false } }) / HttpRouter.serve(app, { routerConfig: ... }), or provide HttpRouter.RouterConfig.
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 10h 36m
- Merged PRs (30d)
- 449
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
Effect-TS/effect#8863 · 1 comment ·
Maintainers usually reply within 1 day
-
BrowserWorkerRunner: port finalizer throws when the worker global has no close() (Bun)Possibly taken @santiago-ramos-02 claimed this 7 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Effect-TS/effect#8635 · 3 comments ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Effect-TS/effect#8101 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 67/100
Effect-TS/effect#8860 · 1 reaction ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 48/100
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
refactor
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
tomnewport/memprot-topo#55 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
WalletConnect/walletconnect-monorepo#7368 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
BU-Spark/se-chem-apll#47 ·
-
embed: handleTurboSignMessage header comment says the signing page posts to '*' (it never does)Opendocumentation
Difficulty 2/5 Under an hour Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 Half a day Newbie friendliness 70/100
udistrital/paginaweb_root#23 ·