@@ -43,7 +43,7 @@ export interface ProxyError {
4343}
4444
4545// Hop-by-hop headers that should not be forwarded
46- const HOP_BY_HOP_HEADERS = [
46+ const HOP_BY_HOP_HEADERS = new Set ( [
4747 'connection' ,
4848 'keep-alive' ,
4949 'proxy-authenticate' ,
@@ -52,14 +52,32 @@ const HOP_BY_HOP_HEADERS = [
5252 'trailer' ,
5353 'transfer-encoding' ,
5454 'upgrade' ,
55- ] ;
55+ ] ) ;
56+
57+ /**
58+ * Parses the Connection header to extract dynamically-nominated hop-by-hop
59+ * header names (RFC 7230 Section 6.1). These headers are specific to the
60+ * current connection and must not be forwarded by proxies.
61+ */
62+ function getDynamicHopByHopHeaders ( headers : Headers ) : Set < string > {
63+ const connectionValue = headers . get ( 'connection' ) ;
64+ if ( ! connectionValue ) {
65+ return new Set ( ) ;
66+ }
67+ return new Set (
68+ connectionValue
69+ . split ( ',' )
70+ . map ( h => h . trim ( ) . toLowerCase ( ) )
71+ . filter ( h => h . length > 0 ) ,
72+ ) ;
73+ }
5674
5775// Headers to strip from proxied responses. fetch() auto-decompresses
5876// response bodies, so Content-Encoding no longer describes the body
5977// and Content-Length reflects the compressed size. We request identity
6078// encoding upstream to avoid the double compression pass, but strip
6179// these defensively since servers may ignore Accept-Encoding: identity.
62- const RESPONSE_HEADERS_TO_STRIP = [ 'content-encoding' , 'content-length' ] ;
80+ const RESPONSE_HEADERS_TO_STRIP = new Set ( [ 'content-encoding' , 'content-length' ] ) ;
6381
6482/**
6583 * Derives the Frontend API URL from a publishable key.
@@ -114,6 +132,7 @@ function createErrorResponse(code: ProxyErrorCode, message: string, status: numb
114132 status,
115133 headers : {
116134 'Content-Type' : 'application/json' ,
135+ 'Cache-Control' : 'no-store' ,
117136 } ,
118137 } ) ;
119138}
@@ -230,9 +249,12 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend
230249 // Build headers for the proxied request
231250 const headers = new Headers ( ) ;
232251
233- // Copy original headers, excluding hop-by-hop headers
252+ // Copy original headers, excluding hop-by-hop headers and any
253+ // dynamically-nominated hop-by-hop headers listed in the Connection header (RFC 7230 Section 6.1).
254+ const dynamicHopByHop = getDynamicHopByHopHeaders ( request . headers ) ;
234255 request . headers . forEach ( ( value , key ) => {
235- if ( ! HOP_BY_HOP_HEADERS . includes ( key . toLowerCase ( ) ) ) {
256+ const lower = key . toLowerCase ( ) ;
257+ if ( ! HOP_BY_HOP_HEADERS . has ( lower ) && ! dynamicHopByHop . has ( lower ) ) {
236258 headers . set ( key , value ) ;
237259 }
238260 } ) ;
@@ -270,31 +292,39 @@ export async function clerkFrontendApiProxy(request: Request, options?: Frontend
270292 headers . set ( 'X-Forwarded-For' , clientIp ) ;
271293 }
272294
273- // Determine if request has a body
274- const hasBody = [ 'POST' , 'PUT' , 'PATCH' ] . includes ( request . method ) ;
295+ // Determine if request has a body (handles DELETE-with-body and any other method)
296+ const hasBody = request . body !== null ;
275297
276298 try {
277299 // Make the proxied request
300+ // TODO: Consider adding AbortSignal.timeout(30_000) via AbortSignal.any()
278301 const fetchOptions : RequestInit = {
279302 method : request . method ,
280303 headers,
281304 redirect : 'manual' ,
282- // @ts -expect-error - duplex is required for streaming bodies but not in all TS definitions
283- duplex : hasBody ? 'half' : undefined ,
305+ signal : request . signal ,
284306 } ;
285307
286- // Only include body for methods that support it
287- if ( hasBody && request . body ) {
308+ // Only set duplex when body is present (required for streaming bodies)
309+ if ( hasBody ) {
310+ // @ts -expect-error - duplex is required for streaming bodies, but not present on the RequestInit type from undici
311+ fetchOptions . duplex = 'half' ;
288312 fetchOptions . body = request . body ;
289313 }
290314
291315 const response = await fetch ( targetUrl . toString ( ) , fetchOptions ) ;
292316
293- // Build response headers, excluding hop-by-hop and encoding headers
317+ // Build response headers, excluding hop-by-hop and encoding headers.
318+ // Also strip dynamically-nominated hop-by-hop headers from the response Connection header.
319+ const responseDynamicHopByHop = getDynamicHopByHopHeaders ( response . headers ) ;
294320 const responseHeaders = new Headers ( ) ;
295321 response . headers . forEach ( ( value , key ) => {
296322 const lower = key . toLowerCase ( ) ;
297- if ( ! HOP_BY_HOP_HEADERS . includes ( lower ) && ! RESPONSE_HEADERS_TO_STRIP . includes ( lower ) ) {
323+ if (
324+ ! HOP_BY_HOP_HEADERS . has ( lower ) &&
325+ ! RESPONSE_HEADERS_TO_STRIP . has ( lower ) &&
326+ ! responseDynamicHopByHop . has ( lower )
327+ ) {
298328 if ( lower === 'set-cookie' ) {
299329 responseHeaders . append ( key , value ) ;
300330 } else {
0 commit comments