Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/node/src/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ it('toStandardUrl', () => {
expect(toStandardUrl({ url: '/foo?bar=1#baz' } as any)).toBe('/foo?bar=1#baz')
expect(toStandardUrl({ url: '/', originalUrl: '/foo?bar=2#baz' } as any)).toBe('/foo?bar=2#baz')
expect(toStandardUrl({ url: 'base' } as any)).toBe('/base')
expect(toStandardUrl({ url: 'http://127.0.0.1:3000/ping' } as any)).toBe('/ping')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth pinning the scheme-guard fallback branch: every new case here is an http(s) URL, so parsed.protocol !== 'http:' && parsed.protocol !== 'https:' in fromAbsoluteHttpUrl is the only branch still uncovered — new URL('ftp://example.com/x') parses fine, and dropping that guard would silently change it from /ftp://example.com/x to /x with no test noticing. Adding expect(toStandardUrl({ url: 'ftp://example.com/x' } as any)).toBe('/ftp://example.com/x') would lock in the documented "non-http schemes still get the previous /${url} behavior".

expect(toStandardUrl({ url: 'http://example.com/foo?bar=1' } as any)).toBe('/foo?bar=1')
expect(toStandardUrl({ url: 'https://example.com/foo#h' } as any)).toBe('/foo#h')
expect(toStandardUrl({ url: 'HTTP://EXAMPLE.COM/Foo' } as any)).toBe('/Foo')
expect(toStandardUrl({ url: '/', originalUrl: 'http://127.0.0.1:80/foo?x=1' } as any)).toBe('/foo?x=1')
})
28 changes: 27 additions & 1 deletion packages/node/src/url.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import type { StandardUrl } from '@standardserver/core'
import type { NodeHttpRequest } from './types'

function fromAbsoluteHttpUrl(url: string): StandardUrl | undefined {
try {
const parsed = new URL(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return undefined
}

const pathname = `${parsed.pathname.startsWith('/') ? '' : '/'}${parsed.pathname}` as `/${string}`
return `${pathname}${parsed.search}${parsed.hash}`
}
catch {
return undefined
}
}

export function toStandardUrl(req: NodeHttpRequest): StandardUrl {
// prefer originalUrl over url, especially useful in express.js middleware
const url = req.originalUrl ?? req.url ?? '/'
return `${url.startsWith('/') ? '' : '/'}${url}` as `/${string}`

if (url.startsWith('/')) {
return url as StandardUrl
}

// RFC 9112 absolute-form. Fetch adapter uses URL.pathname + search + hash.
const fromAbsolute = fromAbsoluteHttpUrl(url)
if (fromAbsolute !== undefined) {
return fromAbsolute
}

return `/${url}` as StandardUrl
}
Loading