Skip to content

Commit c47e54b

Browse files
committed
feat(core): Add support for x-forwarded-host and x-forwarded-proto headers
1 parent f0cad82 commit c47e54b

2 files changed

Lines changed: 199 additions & 2 deletions

File tree

packages/core/src/utils/request.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,15 @@ export function httpRequestToRequestData(request: {
7474
};
7575
}): RequestEventData {
7676
const headers = request.headers || {};
77-
const host = typeof headers.host === 'string' ? headers.host : undefined;
78-
const protocol = request.protocol || (request.socket?.encrypted ? 'https' : 'http');
77+
78+
// Check for x-forwarded-host first, then fall back to host header
79+
const forwardedHost = typeof headers['x-forwarded-host'] === 'string' ? headers['x-forwarded-host'] : undefined;
80+
const host = forwardedHost || (typeof headers.host === 'string' ? headers.host : undefined);
81+
82+
// Check for x-forwarded-proto first, then fall back to existing protocol detection
83+
const forwardedProto = typeof headers['x-forwarded-proto'] === 'string' ? headers['x-forwarded-proto'] : undefined;
84+
const protocol = forwardedProto || request.protocol || (request.socket?.encrypted ? 'https' : 'http');
85+
7986
const url = request.url || '';
8087

8188
const absoluteUrl = getAbsoluteUrl({

packages/core/test/lib/utils/request.test.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,196 @@ describe('request utils', () => {
198198
data: { xx: 'a', yy: 'z' },
199199
});
200200
});
201+
202+
describe('x-forwarded headers support', () => {
203+
it('should prioritize x-forwarded-proto header over explicit protocol parameter', () => {
204+
const actual = httpRequestToRequestData({
205+
url: '/test',
206+
headers: {
207+
host: 'example.com',
208+
'x-forwarded-proto': 'https',
209+
},
210+
protocol: 'http',
211+
});
212+
213+
expect(actual).toEqual({
214+
url: 'https://example.com/test',
215+
headers: {
216+
host: 'example.com',
217+
'x-forwarded-proto': 'https',
218+
},
219+
});
220+
});
221+
222+
it('should prioritize x-forwarded-proto header over socket encryption detection', () => {
223+
const actual = httpRequestToRequestData({
224+
url: '/test',
225+
headers: {
226+
host: 'example.com',
227+
'x-forwarded-proto': 'https',
228+
},
229+
socket: {
230+
encrypted: false,
231+
},
232+
});
233+
234+
expect(actual).toEqual({
235+
url: 'https://example.com/test',
236+
headers: {
237+
host: 'example.com',
238+
'x-forwarded-proto': 'https',
239+
},
240+
});
241+
});
242+
243+
it('should prioritize x-forwarded-host header over standard host header', () => {
244+
const actual = httpRequestToRequestData({
245+
url: '/test',
246+
headers: {
247+
host: 'localhost:3000',
248+
'x-forwarded-host': 'example.com',
249+
'x-forwarded-proto': 'https',
250+
},
251+
});
252+
253+
expect(actual).toEqual({
254+
url: 'https://example.com/test',
255+
headers: {
256+
host: 'localhost:3000',
257+
'x-forwarded-host': 'example.com',
258+
'x-forwarded-proto': 'https',
259+
},
260+
});
261+
});
262+
263+
it('should construct URL correctly when both x-forwarded-proto and x-forwarded-host are present', () => {
264+
const actual = httpRequestToRequestData({
265+
method: 'POST',
266+
url: '/api/test?param=value',
267+
headers: {
268+
host: 'localhost:3000',
269+
'x-forwarded-host': 'api.example.com',
270+
'x-forwarded-proto': 'https',
271+
'content-type': 'application/json',
272+
},
273+
protocol: 'http',
274+
});
275+
276+
expect(actual).toEqual({
277+
method: 'POST',
278+
url: 'https://api.example.com/api/test?param=value',
279+
query_string: 'param=value',
280+
headers: {
281+
host: 'localhost:3000',
282+
'x-forwarded-host': 'api.example.com',
283+
'x-forwarded-proto': 'https',
284+
'content-type': 'application/json',
285+
},
286+
});
287+
});
288+
289+
it('should fall back to standard headers when x-forwarded headers are not present', () => {
290+
const actual = httpRequestToRequestData({
291+
url: '/test',
292+
headers: {
293+
host: 'example.com',
294+
},
295+
protocol: 'https',
296+
});
297+
298+
expect(actual).toEqual({
299+
url: 'https://example.com/test',
300+
headers: {
301+
host: 'example.com',
302+
},
303+
});
304+
});
305+
306+
it('should ignore x-forwarded headers when they contain non-string values', () => {
307+
const actual = httpRequestToRequestData({
308+
url: '/test',
309+
headers: {
310+
host: 'example.com',
311+
'x-forwarded-host': ['forwarded.example.com'] as any,
312+
'x-forwarded-proto': ['https'] as any,
313+
},
314+
protocol: 'http',
315+
});
316+
317+
expect(actual).toEqual({
318+
url: 'http://example.com/test',
319+
headers: {
320+
host: 'example.com',
321+
},
322+
});
323+
});
324+
325+
it('should correctly transform localhost request to public URL using x-forwarded headers', () => {
326+
const actual = httpRequestToRequestData({
327+
method: 'GET',
328+
url: '/',
329+
headers: {
330+
host: 'localhost:3000',
331+
'x-forwarded-proto': 'https',
332+
'x-forwarded-host': 'example.com',
333+
},
334+
});
335+
336+
expect(actual).toEqual({
337+
method: 'GET',
338+
url: 'https://example.com/',
339+
headers: {
340+
host: 'localhost:3000',
341+
'x-forwarded-proto': 'https',
342+
'x-forwarded-host': 'example.com',
343+
},
344+
});
345+
});
346+
347+
it('should respect x-forwarded-proto even when it downgrades from encrypted socket', () => {
348+
const actual = httpRequestToRequestData({
349+
url: '/test',
350+
headers: {
351+
host: 'example.com',
352+
'x-forwarded-proto': 'http',
353+
},
354+
socket: {
355+
encrypted: true,
356+
},
357+
});
358+
359+
expect(actual).toEqual({
360+
url: 'http://example.com/test',
361+
headers: {
362+
host: 'example.com',
363+
'x-forwarded-proto': 'http',
364+
},
365+
});
366+
});
367+
368+
it('should preserve query parameters when constructing URL with x-forwarded headers', () => {
369+
const actual = httpRequestToRequestData({
370+
method: 'GET',
371+
url: '/search?q=test&category=api',
372+
headers: {
373+
host: 'localhost:8080',
374+
'x-forwarded-host': 'search.example.com',
375+
'x-forwarded-proto': 'https',
376+
},
377+
});
378+
379+
expect(actual).toEqual({
380+
method: 'GET',
381+
url: 'https://search.example.com/search?q=test&category=api',
382+
query_string: 'q=test&category=api',
383+
headers: {
384+
host: 'localhost:8080',
385+
'x-forwarded-host': 'search.example.com',
386+
'x-forwarded-proto': 'https',
387+
},
388+
});
389+
});
390+
});
201391
});
202392

203393
describe('extractQueryParamsFromUrl', () => {

0 commit comments

Comments
 (0)