| name | fastify-url-special-characters |
| description | Fix test failures when validating route parameters with special characters in Fastify.
Use when: (1) Tests expect 400 but get 404 for invalid route parameters,
(2) Special characters like # @ are being stripped from URLs before handler runs,
(3) Route parameter validation tests fail unexpectedly.
Covers Fastify URL parsing behavior and how to test parameter validation correctly.
|
| author | Claude Code |
| version | 1.0.0 |
| date | 2026-01-25T00:00:00.000Z |
Fastify URL Special Characters in Route Parameters
Problem
When testing route parameter validation in Fastify, tests may fail because certain
special characters are handled at the routing level before your handler runs.
You expect 400 Bad Request from your validation but get 404 Not Found because
Fastify never matched your route.
Context / Trigger Conditions
- Test expects
400 status code but receives 404
- Testing route parameter validation with special characters
- Using Fastify with dynamic route parameters like
/gateways/:gatewayId/mcp
- Characters like
#, ?, or URL-encoded values in test URLs
Example test that fails unexpectedly:
const response = await server.inject({
method: "POST",
url: "/gateways/invalid@gateway#id!/mcp",
payload: validPayload,
});
expect(response.statusCode).toBe(400);
Root Cause
Fastify (and most HTTP frameworks) parse URLs according to RFC 3986:
-
# (hash/fragment): Everything after # is treated as a fragment and stripped
/gateways/invalid@gateway#id/mcp → /gateways/invalid@gateway (route doesn't match)
-
? (query string): Everything after ? is parsed as query parameters
/gateways/test?id/mcp → route is /gateways/test with query id/mcp
-
URL encoding: Some characters are decoded before routing
%20 (space) is passed through to handler
%2F (/) may cause routing issues
Solution
Step 1: Identify Which Characters Fastify Passes Through
Characters that reach your handler (can be validated):
- Letters, numbers, hyphens, underscores:
a-z, A-Z, 0-9, -, _
- Dots:
. (passed through)
- Spaces (URL-encoded as
%20): decoded and passed through
@ symbol: passed through
Characters that DON'T reach your handler:
# - Treated as fragment start, URL truncated
? - Treated as query string start
/ - Path separator (even when encoded)
Step 2: Update Tests to Use Passable Invalid Characters
url: "/gateways/invalid@gateway#id!/mcp";
url: "/gateways/invalid.gateway.id/mcp";
url: "/gateways/invalid gateway/mcp";
Step 3: Test Each Validation Rule Separately
it("should return 400 for invalid gateway ID format (with dot)", async () => {
const response = await server.inject({
method: "POST",
url: "/gateways/invalid.gateway.id/mcp",
payload: validPayload,
});
expect(response.statusCode).toBe(400);
});
it("should return 400 for gateway ID exceeding max length", async () => {
const longId = "a".repeat(200);
const response = await server.inject({
method: "POST",
url: `/gateways/${longId}/mcp`,
payload: validPayload,
});
expect([400, 404]).toContain(response.statusCode);
});
Verification
Test that your validation pattern correctly rejects the characters you care about:
const GATEWAY_ID_PATTERN = /^[a-zA-Z0-9_-]{1,128}$/;
console.log(GATEWAY_ID_PATTERN.test("invalid.gateway"));
console.log(GATEWAY_ID_PATTERN.test("invalid gateway"));
console.log(GATEWAY_ID_PATTERN.test(""));
console.log(GATEWAY_ID_PATTERN.test("valid-gateway"));
console.log(GATEWAY_ID_PATTERN.test("valid_gateway_123"));
Example
Real-world fix from MCP Proxy service:
it("should return 400 for invalid gateway ID format", async () => {
const response = await server.inject({
method: "POST",
url: "/gateways/invalid@gateway#id!/mcp",
payload: validJsonRpcRequest,
});
expect(response.statusCode).toBe(400);
});
it("should return 400 for invalid gateway ID format (with dot)", async () => {
const response = await server.inject({
method: "POST",
url: "/gateways/invalid.gateway.id/mcp",
payload: validJsonRpcRequest,
});
expect(response.statusCode).toBe(400);
});
Notes
- This behavior is consistent across most HTTP frameworks, not just Fastify
- The RFC 3986 URL spec defines this parsing behavior
- If you need to accept special characters in route parameters, consider:
- Base64 encoding the parameter value
- Using query parameters instead of path parameters
- URL-encoding and decoding manually
References