From 094374d4ccb9469b9ec462ec946c2e1707d72388 Mon Sep 17 00:00:00 2001 From: justxd22 Date: Mon, 20 Apr 2026 01:12:07 +0200 Subject: [PATCH 1/3] fix: return HTML for browser Accept headers on root route (#532) --- .../request-handlers/root-request-handler.ts | 31 +++++++++++++++++-- src/routes/index.ts | 5 ++- .../features/nip-11/nip-11.feature | 5 +++ .../features/nip-11/nip-11.feature.ts | 10 ++++++ .../root-request-handler.spec.ts | 22 ++++++++++++- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index d3502fa3..1d8ff4a5 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -1,4 +1,3 @@ -import accepts from 'accepts' import { NextFunction, Request, Response } from 'express' import { path, pathEq } from 'ramda' import { createSettings } from '../../factories/settings-factory' @@ -8,10 +7,38 @@ import { fromBech32 } from '../../utils/transform' import { getTemplate } from '../../utils/template-cache' import packageJson from '../../../package.json' +export const hasExplicitNostrJsonAcceptHeader = (request: Request): boolean => { + const acceptHeader = request.headers.accept + + if (!acceptHeader) { + return false + } + + return acceptHeader.split(',').some((token) => { + const [mediaType, ...params] = token + .split(';') + .map((value) => value.trim().toLowerCase()) + + if (mediaType !== 'application/nostr+json') { + return false + } + + const quality = params.find((param) => param.startsWith('q=')) + + if (!quality) { + return true + } + + const qValue = Number.parseFloat(quality.slice(2)) + + return !Number.isNaN(qValue) && qValue > 0 + }) +} + export const rootRequestHandler = (request: Request, response: Response, next: NextFunction) => { const settings = createSettings() - if (accepts(request).type(['application/nostr+json'])) { + if (hasExplicitNostrJsonAcceptHeader(request)) { const { info: { name, description, pubkey: rawPubkey, contact, relay_url }, } = settings diff --git a/src/routes/index.ts b/src/routes/index.ts index c4c2460b..8d29bdbb 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,4 +1,3 @@ -import accepts from 'accepts' import express from 'express' import { nodeinfo21Handler, nodeinfoHandler } from '../handlers/request-handlers/nodeinfo-handler' @@ -9,12 +8,12 @@ import { getPrivacyRequestHandler } from '../handlers/request-handlers/get-priva import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler' import invoiceRouter from './invoices' import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware' -import { rootRequestHandler } from '../handlers/request-handlers/root-request-handler' +import { hasExplicitNostrJsonAcceptHeader, rootRequestHandler } from '../handlers/request-handlers/root-request-handler' const router = express.Router() router.use((req, res, next) => { - if (req.method === 'GET' && accepts(req).type(['application/nostr+json'])) { + if (req.method === 'GET' && hasExplicitNostrJsonAcceptHeader(req)) { return rootRequestHandler(req, res, next) } next() diff --git a/test/integration/features/nip-11/nip-11.feature b/test/integration/features/nip-11/nip-11.feature index 6e1bd4c0..bdd8e928 100644 --- a/test/integration/features/nip-11/nip-11.feature +++ b/test/integration/features/nip-11/nip-11.feature @@ -14,6 +14,11 @@ Feature: NIP-11 Then the response Content-Type does not include "application/nostr+json" And the response body is not a relay information document + Scenario: Relay serves HTML for typical browser Accept header + When a browser requests the root path + Then the response Content-Type includes "text/html" + And the response body is not a relay information document + Scenario: Relay information document reports max_filters from settings When a client requests the relay information document Then the limitation object contains a max_filters field diff --git a/test/integration/features/nip-11/nip-11.feature.ts b/test/integration/features/nip-11/nip-11.feature.ts index 79aa1e13..d2926f55 100644 --- a/test/integration/features/nip-11/nip-11.feature.ts +++ b/test/integration/features/nip-11/nip-11.feature.ts @@ -29,6 +29,16 @@ When('a client requests the root path with Accept header {string}', async functi this.parameters.httpResponse = response }) +When('a browser requests the root path', async function(this: World>) { + const response: AxiosResponse = await axios.get(BASE_URL, { + headers: { + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + }, + validateStatus: () => true, + }) + this.parameters.httpResponse = response +}) + Then('the response status is {int}', function(this: World>, status: number) { expect(this.parameters.httpResponse.status).to.equal(status) }) diff --git a/test/unit/handlers/request-handlers/root-request-handler.spec.ts b/test/unit/handlers/request-handlers/root-request-handler.spec.ts index d10c2e0b..017d515f 100644 --- a/test/unit/handlers/request-handlers/root-request-handler.spec.ts +++ b/test/unit/handlers/request-handlers/root-request-handler.spec.ts @@ -7,7 +7,10 @@ const { expect } = chai import * as settingsFactory from '../../../../src/factories/settings-factory' import * as templateCache from '../../../../src/utils/template-cache' -import { rootRequestHandler } from '../../../../src/handlers/request-handlers/root-request-handler' +import { + hasExplicitNostrJsonAcceptHeader, + rootRequestHandler, +} from '../../../../src/handlers/request-handlers/root-request-handler' const baseSettings = { info: { @@ -40,6 +43,23 @@ const settingsWithFee = { }, } +describe('hasExplicitNostrJsonAcceptHeader', () => { + it('returns true for explicit application/nostr+json', () => { + expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: 'application/nostr+json' } } as any)).to.equal(true) + }) + + it('returns false for typical browser Accept header', () => { + const browserAcceptHeader = + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' + + expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: browserAcceptHeader } } as any)).to.equal(false) + }) + + it('returns false when q=0 for application/nostr+json', () => { + expect(hasExplicitNostrJsonAcceptHeader({ headers: { accept: 'application/nostr+json;q=0' } } as any)).to.equal(false) + }) +}) + describe('rootRequestHandler', () => { let createSettingsStub: sinon.SinonStub let getTemplateStub: sinon.SinonStub From feebb6306685b5c3a506d9caf3cbb03fa6c11af6 Mon Sep 17 00:00:00 2001 From: justxd22 Date: Mon, 20 Apr 2026 01:16:54 +0200 Subject: [PATCH 2/3] fix: missing changeset --- .changeset/red-dancers-ask.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .changeset/red-dancers-ask.md diff --git a/.changeset/red-dancers-ask.md b/.changeset/red-dancers-ask.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/red-dancers-ask.md @@ -0,0 +1,2 @@ +--- +--- From a65abe13d2f78cd01ac3cf8eb998759cf17ea1b3 Mon Sep 17 00:00:00 2001 From: justxd22 Date: Mon, 20 Apr 2026 01:23:32 +0200 Subject: [PATCH 3/3] fix: scope NIP-11 accept routing to root and handle array Accept headers (#532) --- src/handlers/request-handlers/root-request-handler.ts | 4 +++- src/routes/index.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/handlers/request-handlers/root-request-handler.ts b/src/handlers/request-handlers/root-request-handler.ts index 1d8ff4a5..5aa2b265 100644 --- a/src/handlers/request-handlers/root-request-handler.ts +++ b/src/handlers/request-handlers/root-request-handler.ts @@ -14,7 +14,9 @@ export const hasExplicitNostrJsonAcceptHeader = (request: Request): boolean => { return false } - return acceptHeader.split(',').some((token) => { + const acceptHeaderValue = Array.isArray(acceptHeader) ? acceptHeader.join(',') : acceptHeader + + return acceptHeaderValue.split(',').some((token) => { const [mediaType, ...params] = token .split(';') .map((value) => value.trim().toLowerCase()) diff --git a/src/routes/index.ts b/src/routes/index.ts index 8d29bdbb..28f94336 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -13,7 +13,7 @@ import { hasExplicitNostrJsonAcceptHeader, rootRequestHandler } from '../handler const router = express.Router() router.use((req, res, next) => { - if (req.method === 'GET' && hasExplicitNostrJsonAcceptHeader(req)) { + if (req.method === 'GET' && req.path === '/' && hasExplicitNostrJsonAcceptHeader(req)) { return rootRequestHandler(req, res, next) } next()