diff --git a/src/routing/server.ts b/src/routing/server.ts index 441ac7c..b1c419e 100644 --- a/src/routing/server.ts +++ b/src/routing/server.ts @@ -3,7 +3,7 @@ import { DatapromptStore } from '../core/dataprompt.js'; import { events } from '../core/events.js'; import { getLogManager } from '../utils/logging.js'; import { createPromptFlow, FlowDefinition } from './flow-builder.js'; -import { createRequestContext } from '../utils/helpers/request.js'; +import { createRequestContext, convertHeaders } from '../utils/helpers/request.js'; export interface DatapromptRoute { flowDef: FlowDefinition; @@ -24,8 +24,7 @@ function createRouteHandler({ const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`; const standardRequest = new Request(url, { method: req.method, - // TODO: Improve type safety for headers. req.headers can contain string[] which might not be compatible with Record. - headers: req.headers as Record, + headers: convertHeaders(req.headers), body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined, }); diff --git a/src/utils/helpers/request.ts b/src/utils/helpers/request.ts index cff28dc..569ee23 100644 --- a/src/utils/helpers/request.ts +++ b/src/utils/helpers/request.ts @@ -1,5 +1,27 @@ import { RequestContext, RequestContextSchema } from "../../core/interfaces.js"; import { MatchResult } from "../../routing/route-matcher.js"; +import { IncomingHttpHeaders } from 'http'; + +/** + * Converts Node.js/Express headers to Fetch API Headers. + * Handles strings, string arrays, and undefined values safely. + * @param headers Node.js IncomingHttpHeaders + * @returns Standard Fetch API Headers object + */ +export function convertHeaders(headers: IncomingHttpHeaders): Headers { + const fetchHeaders = new Headers(); + for (const [key, value] of Object.entries(headers)) { + if (value === undefined) { + continue; + } + if (Array.isArray(value)) { + value.forEach((v) => fetchHeaders.append(key, v)); + } else { + fetchHeaders.append(key, value); + } + } + return fetchHeaders; +} /** * A universal helper to create a valid RequestContext from various inputs. diff --git a/tests/unit/request.helper.test.ts b/tests/unit/request.helper.test.ts index 909438e..e5415a5 100644 --- a/tests/unit/request.helper.test.ts +++ b/tests/unit/request.helper.test.ts @@ -1,7 +1,49 @@ import { describe, it, expect } from 'vitest'; -import { createRequestContext } from '../../src/utils/helpers/request.js'; +import { createRequestContext, convertHeaders } from '../../src/utils/helpers/request.js'; import { RequestContext } from '../../src/core/interfaces'; import { MatchResult } from '../../src/routing/route-matcher.js'; +import { IncomingHttpHeaders } from 'http'; + +describe('convertHeaders', () => { + it('should correctly handle string headers', () => { + const headers: IncomingHttpHeaders = { + 'content-type': 'application/json', + 'x-api-key': '12345' + }; + const fetchHeaders = convertHeaders(headers); + expect(fetchHeaders.get('content-type')).toBe('application/json'); + expect(fetchHeaders.get('x-api-key')).toBe('12345'); + }); + + it('should correctly handle array headers', () => { + const headers: IncomingHttpHeaders = { + 'x-custom-list': ['item1', 'item2'] + }; + const fetchHeaders = convertHeaders(headers); + // Headers.get returns values comma-separated + expect(fetchHeaders.get('x-custom-list')).toBe('item1, item2'); + }); + + it('should ignore undefined headers', () => { + const headers: IncomingHttpHeaders = { + 'x-valid': 'true', + 'x-undefined': undefined + }; + const fetchHeaders = convertHeaders(headers); + expect(fetchHeaders.has('x-valid')).toBe(true); + expect(fetchHeaders.has('x-undefined')).toBe(false); + }); + + it('should handle mixed string and array headers', () => { + const headers: IncomingHttpHeaders = { + 'x-single': 'value', + 'x-multiple': ['v1', 'v2'] + }; + const fetchHeaders = convertHeaders(headers); + expect(fetchHeaders.get('x-single')).toBe('value'); + expect(fetchHeaders.get('x-multiple')).toBe('v1, v2'); + }); +}); describe('createRequestContext', () => { it('should handle a simple URL string', async () => {