Skip to content
Merged
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: 2 additions & 3 deletions src/routing/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string, string>.
headers: req.headers as Record<string, string>,
headers: convertHeaders(req.headers),
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
});

Expand Down
22 changes: 22 additions & 0 deletions src/utils/helpers/request.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
44 changes: 43 additions & 1 deletion tests/unit/request.helper.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down