| name | nip98-url-query-param-mismatch |
| description | Fix NIP-98 HTTP authentication 401 errors caused by URL mismatch between the `u` tag
and what the server expects. Use when: (1) NIP-98 auth returns 401 with "URL mismatch" error,
(2) Token creation succeeds but server rejects authentication, (3) Server logs show
expected vs actual URL difference. CRITICAL: Server implementations vary - some strip
query params, some don't. Check the actual error message to determine which behavior applies.
|
| author | Claude Code |
| version | 1.1.0 |
| date | "2026-02-01T00:00:00.000Z" |
NIP-98 URL Query Parameter Mismatch
Problem
NIP-98 HTTP authentication fails with 401 Unauthorized even though the token is created
successfully and the signature is valid. The actual cause is a URL mismatch between
the u tag in the signed event and what the server expects.
Context / Trigger Conditions
- Server returns 401 Unauthorized for NIP-98 protected endpoints
- Logs show token creation succeeded (event signed and validated)
- Error response contains "URL mismatch" with expected vs actual URLs
- The URLs differ only by query parameters
Example error response:
{"error":"Auth failed: URL mismatch: expected .../notifications, got .../notifications?limit=50"}
Root Cause
The NIP-98 spec says:
The u tag MUST be exactly the same as the absolute request URL (including query parameters).
But server implementations vary:
- Some servers follow the spec strictly (require query params in
u tag)
- Some servers normalize URLs by stripping query params before validation
- You MUST match what your specific server does
Solution
Step 1: Add logging to see the actual error
} else if (response.statusCode == 401) {
Log.error(
'NIP-98 auth failed (401)\n'
'URL: $url\n'
'Response: ${response.body}', // <-- This reveals the actual issue
...
);
}
Step 2: Check the error message
- If error says "expected .../path?params, got .../path" → Include query params
- If error says "expected .../path, got .../path?params" → Strip query params
Step 3: Adjust URL normalization accordingly
For servers that STRIP query params (like Divine Relay):
final uri = Uri.parse(url);
// Server strips query params before NIP-98 validation
final normalizedUrl = '${uri.scheme}://${uri.host}${uri.path}';
For servers that REQUIRE query params (per NIP-98 spec):
final uri = Uri.parse(url);
final normalizedUrl = uri.hasQuery
? '${uri.scheme}://${uri.host}${uri.path}?${uri.query}'
: '${uri.scheme}://${uri.host}${uri.path}';
Verification
- After the fix, the URLs in the
u tag should match what server expects
- The 401 error should be replaced by successful authentication (200)
- Check logs to confirm URL matching
Example
Divine Relay behavior (strips query params):
- Request URL:
https://relay.dvines.org/api/users/abc/notifications?limit=50
- Server validates against:
https://relay.dvines.org/api/users/abc/notifications
u tag must be: https://relay.dvines.org/api/users/abc/notifications
Notes
- The NIP-98 spec is clear about including query params, but not all servers follow it
- Always check the actual error response to determine server behavior
- When in doubt, try both approaches and see which works
- Consider filing a bug with servers that don't follow the spec
References
- NIP-98 HTTP Auth Specification
- Spec quote: "The
u tag MUST be exactly the same as the absolute request URL (including query parameters)"
- Reality: Server implementations vary, always check actual behavior