33 * Proxy Helper for Hono.
44 */
55
6+ import { HTTPException } from '../../http-exception'
67import type { RequestHeader } from '../../utils/headers'
78
89// https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1
@@ -12,10 +13,14 @@ const hopByHopHeaders = [
1213 'proxy-authenticate' ,
1314 'proxy-authorization' ,
1415 'te' ,
15- 'trailers ' ,
16+ 'trailer ' ,
1617 'transfer-encoding' ,
18+ 'upgrade' ,
1719]
1820
21+ // https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
22+ const ALLOWED_TOKEN_PATTERN = / ^ [ ! # $ % & ' * + \- . 0 - 9 A - Z ^ _ ` a - z | ~ ] + $ /
23+
1924interface ProxyRequestInit extends Omit < RequestInit , 'headers' > {
2025 raw ?: Request
2126 headers ?:
@@ -24,20 +29,54 @@ interface ProxyRequestInit extends Omit<RequestInit, 'headers'> {
2429 | Record < RequestHeader , string | undefined >
2530 | Record < string , string | undefined >
2631 customFetch ?: ( request : Request ) => Promise < Response >
32+ /**
33+ * Enable strict RFC 9110 compliance for Connection header processing.
34+ *
35+ * - `false` (default): Ignores Connection header to prevent potential
36+ * Hop-by-Hop Header Injection attacks. Recommended for untrusted clients.
37+ * - `true`: Processes Connection header per RFC 9110 and removes listed headers.
38+ * Only use in trusted environments.
39+ *
40+ * @default false
41+ * @see https://datatracker.ietf.org/doc/html/rfc9110#section-7.6.1
42+ */
43+ strictConnectionProcessing ?: boolean
2744}
2845
2946interface ProxyFetch {
3047 ( input : string | URL | Request , init ?: ProxyRequestInit ) : Promise < Response >
3148}
3249
3350const buildRequestInitFromRequest = (
34- request : Request | undefined
51+ request : Request | undefined ,
52+ strictConnectionProcessing : boolean
3553) : RequestInit & { duplex ?: 'half' } => {
3654 if ( ! request ) {
3755 return { }
3856 }
3957
4058 const headers = new Headers ( request . headers )
59+
60+ if ( strictConnectionProcessing ) {
61+ // https://datatracker.ietf.org/doc/html/rfc9110#section-7.6.1
62+ // Parse Connection header and remove listed headers (MUST per RFC 9110)
63+ const connectionValue = headers . get ( 'connection' )
64+ if ( connectionValue ) {
65+ const headerNames = connectionValue . split ( ',' ) . map ( ( h ) => h . trim ( ) )
66+ // Validate header names per RFC 9110 Section 5.6.2 (token syntax)
67+ const invalidHeaders = headerNames . filter ( ( h ) => ! ALLOWED_TOKEN_PATTERN . test ( h ) )
68+
69+ if ( invalidHeaders . length > 0 ) {
70+ throw new HTTPException ( 400 , {
71+ message : `Invalid Connection header value: ${ invalidHeaders . join ( ', ' ) } ` ,
72+ } )
73+ }
74+ headerNames . forEach ( ( headerName ) => {
75+ headers . delete ( headerName )
76+ } )
77+ }
78+ }
79+
4180 hopByHopHeaders . forEach ( ( header ) => {
4281 headers . delete ( header )
4382 } )
@@ -108,14 +147,26 @@ const preprocessRequestInit = (requestInit: RequestInit): RequestInit => {
108147 * },
109148 * })
110149 * })
150+ *
151+ * // Strict RFC compliance mode (use only in trusted environments)
152+ * app.get('/internal-proxy/:path', (c) => {
153+ * return proxy(`http://${internalServer}/${c.req.param('path')}`, {
154+ * ...c.req,
155+ * strictConnectionProcessing: true,
156+ * })
157+ * })
111158 * ```
112159 */
113160export const proxy : ProxyFetch = async ( input , proxyInit ) => {
114- const { raw, customFetch, ...requestInit } =
115- proxyInit instanceof Request ? { raw : proxyInit } : proxyInit ?? { }
161+ const {
162+ raw,
163+ customFetch,
164+ strictConnectionProcessing = false ,
165+ ...requestInit
166+ } = proxyInit instanceof Request ? { raw : proxyInit } : proxyInit ?? { }
116167
117168 const req = new Request ( input , {
118- ...buildRequestInitFromRequest ( raw ) ,
169+ ...buildRequestInitFromRequest ( raw , strictConnectionProcessing ) ,
119170 ...preprocessRequestInit ( requestInit as RequestInit ) ,
120171 } )
121172 req . headers . delete ( 'accept-encoding' )
0 commit comments