bxo 0.0.5-dev.63 → 0.0.5-dev.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -406,6 +406,76 @@ await app.stop();
406
406
  await app.listen(3000); // Same as app.start(3000)
407
407
  ```
408
408
 
409
+ ### Custom Serve Options
410
+
411
+ BXO allows you to extend the native Bun serve options, giving you full control over the server configuration while maintaining the framework's functionality:
412
+
413
+ ```typescript
414
+ // Basic serve options
415
+ const app = new BXO({
416
+ serve: {
417
+ port: 3000,
418
+ hostname: "0.0.0.0", // Listen on all interfaces
419
+ }
420
+ });
421
+
422
+ // With TLS/HTTPS options
423
+ const app = new BXO({
424
+ serve: {
425
+ port: 443,
426
+ hostname: "localhost",
427
+ // tls: {
428
+ // cert: Bun.file("cert.pem"),
429
+ // key: Bun.file("key.pem"),
430
+ // }
431
+ }
432
+ });
433
+
434
+ // With development options
435
+ const app = new BXO({
436
+ serve: {
437
+ port: 5000,
438
+ development: true, // Enable development mode
439
+ }
440
+ });
441
+
442
+ // With custom error handling
443
+ const app = new BXO({
444
+ serve: {
445
+ port: 6000,
446
+ error: (error) => {
447
+ console.error("Custom error handler:", error);
448
+ return new Response("Something went wrong", { status: 500 });
449
+ }
450
+ }
451
+ });
452
+
453
+ // With custom fetch handler (extends framework functionality)
454
+ const app = new BXO({
455
+ serve: {
456
+ port: 4000,
457
+ // You can override the fetch handler if needed
458
+ // fetch: (request, server) => {
459
+ // // Your custom logic here
460
+ // // You can still call the framework's handler
461
+ // return app.requestHandler.handleRequest(request, server);
462
+ // }
463
+ }
464
+ });
465
+ ```
466
+
467
+ **Available Serve Options:**
468
+ - `port` - Server port (default: 3000)
469
+ - `hostname` - Server hostname (default: 'localhost')
470
+ - `development` - Enable development mode
471
+ - `tls` - TLS/HTTPS configuration
472
+ - `error` - Custom error handler
473
+ - `fetch` - Custom fetch handler (overrides framework's handler)
474
+ - `websocket` - WebSocket configuration
475
+ - And all other [Bun.serve options](https://bun.sh/docs/api/http#bun-serve)
476
+
477
+ The framework will merge your serve options with its own defaults, ensuring that BXO's routing, middleware, and WebSocket functionality continue to work while allowing you to customize the server behavior.
478
+
409
479
  ### Development vs Production
410
480
 
411
481
  ```typescript
@@ -0,0 +1,15 @@
1
+ # serve-react
2
+
3
+ To install dependencies:
4
+
5
+ ```bash
6
+ bun install
7
+ ```
8
+
9
+ To run:
10
+
11
+ ```bash
12
+ bun run index.ts
13
+ ```
14
+
15
+ This project was created using `bun init` in bun v1.2.19. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
@@ -0,0 +1,8 @@
1
+ import { createRoot } from 'react-dom/client';
2
+
3
+ function App() {
4
+ return <div>Hello World</div>;
5
+ }
6
+
7
+ const root = createRoot(document.getElementById('root') as any);
8
+ root.render(<App />);
@@ -0,0 +1,42 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "serve-react",
6
+ "dependencies": {
7
+ "@types/react-dom": "^19.1.9",
8
+ "react": "^19.1.1",
9
+ "react-dom": "^19.1.1",
10
+ },
11
+ "devDependencies": {
12
+ "@types/bun": "latest",
13
+ },
14
+ "peerDependencies": {
15
+ "typescript": "^5",
16
+ },
17
+ },
18
+ },
19
+ "packages": {
20
+ "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
21
+
22
+ "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
23
+
24
+ "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
25
+
26
+ "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
27
+
28
+ "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
29
+
30
+ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
31
+
32
+ "react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
33
+
34
+ "react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
35
+
36
+ "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
37
+
38
+ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
39
+
40
+ "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
41
+ }
42
+ }
@@ -0,0 +1,9 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <body>
5
+ <div id="root"></div>
6
+ </body>
7
+ <script type="module" src="app.tsx"></script>
8
+
9
+ </html>
@@ -0,0 +1,27 @@
1
+ import BXO from "../../src";
2
+ import index from "./index.html"
3
+
4
+ // Create BXO instance with custom serve options
5
+ const app = new BXO({
6
+ serve: {
7
+ port: 4000,
8
+ routes: {
9
+ "/*": index
10
+ }
11
+ // You can add any Bun.serve options here
12
+ // For example, you can add custom headers, TLS options, etc.
13
+ // The framework will merge these with its own defaults
14
+ }
15
+ });
16
+
17
+ // Add routes
18
+ app.get("/", () => {
19
+ return index
20
+ });
21
+
22
+ app.get("/api/status", () => {
23
+ return { status: "ok", message: "BXO is running with custom serve options" };
24
+ });
25
+
26
+ // Start the server
27
+ app.start();
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "serve-react",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "private": true,
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ },
12
+ "dependencies": {
13
+ "@types/react-dom": "^19.1.9",
14
+ "react": "^19.1.1",
15
+ "react-dom": "^19.1.1"
16
+ }
17
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext", "DOM"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.63",
4
+ "version": "0.0.5-dev.65",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "exports": {
package/src/core/bxo.ts CHANGED
@@ -1,12 +1,12 @@
1
- import type {
2
- Route,
3
- WSRoute,
4
- Handler,
5
- WebSocketHandler,
6
- RouteConfig,
7
- LifecycleHooks,
8
- Plugin,
9
- BXOOptions
1
+ import type {
2
+ Route,
3
+ WSRoute,
4
+ Handler,
5
+ WebSocketHandler,
6
+ RouteConfig,
7
+ LifecycleHooks,
8
+ Plugin,
9
+ BXOOptions
10
10
  } from '../types';
11
11
  import { RequestHandler } from '../handlers/request-handler';
12
12
 
@@ -22,9 +22,11 @@ export default class BXO {
22
22
  private serverHostname?: string;
23
23
  private enableValidation: boolean = true;
24
24
  private requestHandler: RequestHandler;
25
+ private serveOptions?: Partial<Bun.ServeFunctionOptions<any, any>>;
25
26
 
26
27
  constructor(options?: BXOOptions) {
27
28
  this.enableValidation = options?.enableValidation ?? true;
29
+ this.serveOptions = options?.serve;
28
30
  this.requestHandler = new RequestHandler(
29
31
  this._routes,
30
32
  this._wsRoutes,
@@ -228,11 +230,27 @@ export default class BXO {
228
230
  await this.hooks.onBeforeStart(this);
229
231
  }
230
232
 
231
- this.server = Bun.serve({
233
+ // Merge user's serve options with framework defaults
234
+ const defaultServeOptions = {
232
235
  port,
233
236
  hostname,
234
- fetch: (request, server) => this.requestHandler.handleRequest(request, server),
235
- websocket: {
237
+ fetch: async (request: Request, server: any): Promise<Response> => {
238
+ const result = await this.requestHandler.handleRequest(request, server);
239
+ return result || new Response('Not Found', { status: 404 });
240
+ },
241
+ };
242
+
243
+ // Merge user options with defaults, allowing user options to override
244
+ const finalServeOptions = {
245
+ ...defaultServeOptions,
246
+ ...this.serveOptions,
247
+ // Ensure our fetch handler is always used unless user explicitly overrides
248
+ fetch: this.serveOptions?.fetch || defaultServeOptions.fetch,
249
+ };
250
+
251
+ // Add websocket support if there are WebSocket routes
252
+ if (this.getAllWSRoutes().length > 0) {
253
+ (finalServeOptions as any).websocket = {
236
254
  message: (ws: any, message: any) => {
237
255
  const handler = ws.data?.handler;
238
256
  if (handler?.onMessage) {
@@ -251,8 +269,10 @@ export default class BXO {
251
269
  handler.onClose(ws, code, reason);
252
270
  }
253
271
  }
254
- }
255
- });
272
+ };
273
+ }
274
+
275
+ this.server = Bun.serve(finalServeOptions as any);
256
276
 
257
277
  // Verify server was created successfully
258
278
  if (!this.server) {
@@ -76,7 +76,8 @@ export class RequestHandler {
76
76
 
77
77
  // Process and return response
78
78
  const internalCookies = getInternalCookies(ctx);
79
- return processResponse(response, ctx, internalCookies, this.enableValidation, route.config);
79
+ const processedResponse = processResponse(response, ctx, internalCookies, this.enableValidation, route.config);
80
+ return processedResponse;
80
81
 
81
82
  } catch (error) {
82
83
  // Run error hooks
@@ -140,6 +140,7 @@ export interface LifecycleHooks {
140
140
  // BXO options interface
141
141
  export interface BXOOptions {
142
142
  enableValidation?: boolean;
143
+ serve?: Partial<Bun.ServeFunctionOptions<any, any>>;
143
144
  }
144
145
 
145
146
  // Plugin interface for middleware-style plugins
@@ -271,6 +271,47 @@ export function cookiesToHeaders(cookies: InternalCookie[]): string[] {
271
271
  });
272
272
  }
273
273
 
274
+ // Special cases for HTTP headers that need specific casing
275
+ const HEADER_CASING_SPECIAL_CASES: Record<string, string> = {
276
+ 'www-authenticate': 'WWW-Authenticate',
277
+ 'content-md5': 'Content-MD5',
278
+ 'dnt': 'DNT',
279
+ 'etag': 'ETag',
280
+ 'te': 'TE',
281
+ 'trailer': 'Trailer',
282
+ 'transfer-encoding': 'Transfer-Encoding',
283
+ 'upgrade': 'Upgrade',
284
+ 'x-forwarded-for': 'X-Forwarded-For',
285
+ 'x-forwarded-proto': 'X-Forwarded-Proto',
286
+ 'x-forwarded-host': 'X-Forwarded-Host',
287
+ 'x-real-ip': 'X-Real-IP',
288
+ 'x-requested-with': 'X-Requested-With',
289
+ 'x-csrf-token': 'X-CSRF-Token',
290
+ 'x-frame-options': 'X-Frame-Options',
291
+ 'x-content-type-options': 'X-Content-Type-Options',
292
+ 'x-xss-protection': 'X-XSS-Protection',
293
+ 'strict-transport-security': 'Strict-Transport-Security',
294
+ 'content-security-policy': 'Content-Security-Policy',
295
+ 'referrer-policy': 'Referrer-Policy',
296
+ 'permissions-policy': 'Permissions-Policy'
297
+ };
298
+
299
+ // Helper function to normalize header casing while preserving special cases
300
+ function normalizeHeaderCase(headerKey: string): string {
301
+ const lowerKey = headerKey.toLowerCase();
302
+ return HEADER_CASING_SPECIAL_CASES[lowerKey] || headerKey;
303
+ }
304
+
305
+ // Helper function to convert Headers object back to plain object while preserving casing
306
+ export function headersToPlainObject(headers: Headers): Record<string, string> {
307
+ const result: Record<string, string> = {};
308
+ headers.forEach((value, key) => {
309
+ // Preserve the original casing from the Headers object
310
+ result[key] = value;
311
+ });
312
+ return result;
313
+ }
314
+
274
315
  // Merge headers with cookies
275
316
  export function mergeHeadersWithCookies(
276
317
  headers: Record<string, string>,
@@ -278,9 +319,10 @@ export function mergeHeadersWithCookies(
278
319
  ): Headers {
279
320
  const newHeaders = new Headers();
280
321
 
281
- // Add regular headers
322
+ // Add regular headers with proper casing
282
323
  Object.entries(headers).forEach(([key, value]) => {
283
- newHeaders.set(key, value);
324
+ const normalizedKey = normalizeHeaderCase(key);
325
+ newHeaders.set(normalizedKey, value);
284
326
  });
285
327
 
286
328
  // Add Set-Cookie headers
@@ -292,6 +334,35 @@ export function mergeHeadersWithCookies(
292
334
  return newHeaders;
293
335
  }
294
336
 
337
+ // Alternative function that returns headers with preserved casing
338
+ export function mergeHeadersWithCookiesPreserveCasing(
339
+ headers: Record<string, string>,
340
+ cookies: InternalCookie[]
341
+ ): Record<string, string> {
342
+ const result: Record<string, string> = { ...headers };
343
+
344
+ // Apply special casing rules
345
+ Object.keys(result).forEach(key => {
346
+ const normalizedKey = normalizeHeaderCase(key);
347
+ if (normalizedKey !== key) {
348
+ result[normalizedKey] = result[key] || '';
349
+ delete result[key];
350
+ }
351
+ });
352
+
353
+ // Add Set-Cookie headers
354
+ if (cookies.length > 0) {
355
+ const cookieHeaders = cookiesToHeaders(cookies);
356
+ // Set-Cookie headers should be separate entries, not joined
357
+ cookieHeaders.forEach((cookieHeader, index) => {
358
+ const key = index === 0 ? 'Set-Cookie' : `Set-Cookie-${index + 1}`;
359
+ result[key] = cookieHeader;
360
+ });
361
+ }
362
+
363
+ return result;
364
+ }
365
+
295
366
  // Create a redirect response
296
367
  export function createRedirectResponse(
297
368
  location: string,
@@ -1,5 +1,5 @@
1
1
  import type { Context, InternalCookie } from '../types';
2
- import { validateResponse, mergeHeadersWithCookies } from './index';
2
+ import { validateResponse, mergeHeadersWithCookies, mergeHeadersWithCookiesPreserveCasing } from './index';
3
3
 
4
4
  // Process and format the response from a route handler
5
5
  export function processResponse(
@@ -13,7 +13,7 @@ export function processResponse(
13
13
  // automatically create a redirect Response so users can call ctx.redirect(...) or set headers without returning.
14
14
  const hasImplicitRedirectIntent = !!ctx.set.redirect
15
15
  || (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400);
16
-
16
+
17
17
  if ((response === undefined || response === null) && hasImplicitRedirectIntent) {
18
18
  const locationFromHeaders = ctx.set.headers && Object.entries(ctx.set.headers).find(([k]) => k.toLowerCase() === 'location')?.[1];
19
19
  const location = ctx.set.redirect?.location || locationFromHeaders;
@@ -28,12 +28,7 @@ export function processResponse(
28
28
 
29
29
  // Handle cookies if any are set
30
30
  if (internalCookies.length > 0) {
31
- const headers = mergeHeadersWithCookies(responseHeaders, internalCookies);
32
- const finalHeaders: Record<string, string> = {};
33
- headers.forEach((value, key) => {
34
- finalHeaders[key] = value;
35
- });
36
- responseHeaders = finalHeaders;
31
+ responseHeaders = mergeHeadersWithCookiesPreserveCasing(responseHeaders, internalCookies);
37
32
  }
38
33
 
39
34
  return new Response(null, {
@@ -77,7 +72,7 @@ export function processResponse(
77
72
  if (response instanceof Response) {
78
73
  // If there are headers set via ctx.set.headers, merge them with the Response headers
79
74
  if (ctx.set.headers && Object.keys(ctx.set.headers).length > 0) {
80
- const newHeaders = mergeHeadersWithCookies(ctx.set.headers, internalCookies);
75
+ const newHeaders = mergeHeadersWithCookiesPreserveCasing(ctx.set.headers, internalCookies);
81
76
 
82
77
  // Create new Response with merged headers
83
78
  return new Response(response.body, {
@@ -124,12 +119,12 @@ export function processResponse(
124
119
 
125
120
  // Handle cookies if any are set
126
121
  if (internalCookies.length > 0) {
127
- const headers = mergeHeadersWithCookies(responseHeaders, internalCookies);
122
+ const headers = mergeHeadersWithCookiesPreserveCasing(responseHeaders, internalCookies);
128
123
 
129
124
  if (typeof response === 'string') {
130
125
  // Only set Content-Type to text/plain if not already set
131
- if (!headers.has('Content-Type')) {
132
- headers.set('Content-Type', 'text/plain');
126
+ if (!headers['Content-Type']) {
127
+ headers['Content-Type'] = 'text/plain';
133
128
  }
134
129
  return new Response(response, {
135
130
  status: ctx.set.status || 200,
@@ -163,15 +158,15 @@ export function processResponse(
163
158
  }
164
159
  return value;
165
160
  }));
166
-
167
- headers.set('Content-Type', 'application/json');
161
+
162
+ headers['Content-Type'] = 'application/json';
168
163
  return new Response(JSON.stringify(serializableResponse), {
169
164
  status: ctx.set.status || 200,
170
165
  headers: headers
171
166
  });
172
167
  }
173
168
 
174
- headers.set('Content-Type', 'application/json');
169
+ headers['Content-Type'] = 'application/json';
175
170
  return new Response(JSON.stringify(response), {
176
171
  status: ctx.set.status || 200,
177
172
  headers: headers
@@ -228,7 +223,7 @@ export function processResponse(
228
223
  }
229
224
  return value;
230
225
  }));
231
-
226
+
232
227
  return new Response(JSON.stringify(serializableResponse), {
233
228
  ...responseInit,
234
229
  headers: {
@@ -258,7 +253,7 @@ export function createErrorResponse(
258
253
  error: errorMessage,
259
254
  ...(details && { details })
260
255
  };
261
-
256
+
262
257
  return new Response(JSON.stringify(response), {
263
258
  status,
264
259
  headers: { 'Content-Type': 'application/json' }
@@ -271,7 +266,7 @@ export function createValidationErrorResponse(
271
266
  status: number = 400
272
267
  ): Response {
273
268
  let validationDetails = undefined;
274
-
269
+
275
270
  // Handle Error objects
276
271
  if (validationError instanceof Error) {
277
272
  if ('errors' in validationError && Array.isArray(validationError.errors)) {
@@ -295,3 +290,4 @@ export function createValidationErrorResponse(
295
290
 
296
291
  return createErrorResponse(errorMessage, status, validationDetails);
297
292
  }
293
+
@@ -426,6 +426,24 @@ describe('BXO Framework Integration', () => {
426
426
  expect(text).toBe('Custom response');
427
427
  expect(response.headers.get('x-custom')).toBe('value');
428
428
  });
429
+
430
+ it('should handle HTML responses with custom Content-Type', async () => {
431
+ app.get('/html', (ctx) => {
432
+ ctx.set.headers['Content-Type'] = 'text/html';
433
+ return `
434
+ <script src="/dist/injected/frontend.js"></script>
435
+ <html><body>Hello World</body></html>
436
+ `;
437
+ });
438
+
439
+ const response = await fetch(`${baseUrl}/html`);
440
+ const text = await response.text();
441
+
442
+ expect(response.status).toBe(200);
443
+ expect(response.headers.get('content-type')).toBe('text/html');
444
+ expect(text).toContain('<script src="/dist/injected/frontend.js"></script>');
445
+ expect(text).toContain('<html><body>Hello World</body></html>');
446
+ });
429
447
  });
430
448
 
431
449
  describe('Status and Headers', () => {
@@ -129,6 +129,32 @@ describe('Response Handler', () => {
129
129
  expect(await result.text()).toBe('Hello World');
130
130
  });
131
131
 
132
+ it('should preserve custom Content-Type for string responses', async () => {
133
+ mockContext.set.headers = { 'Content-Type': 'text/html' };
134
+ const result = processResponse('<html><body>Hello World</body></html>', mockContext, mockInternalCookies, true);
135
+
136
+ expect(result.status).toBe(200);
137
+ expect(result.headers.get('content-type')).toBe('text/html');
138
+ expect(await result.text()).toBe('<html><body>Hello World</body></html>');
139
+ });
140
+
141
+ it('should preserve custom Content-Type for string responses without cookies', async () => {
142
+ mockContext.set.headers = { 'Content-Type': 'text/html' };
143
+ const result = processResponse('<html><body>Hello World</body></html>', mockContext, [], true);
144
+
145
+ expect(result.status).toBe(200);
146
+ expect(result.headers.get('content-type')).toBe('text/html');
147
+ expect(await result.text()).toBe('<html><body>Hello World</body></html>');
148
+ });
149
+
150
+ it('should default to text/plain when no Content-Type is set for string responses', async () => {
151
+ const result = processResponse('Hello World', mockContext, [], true);
152
+
153
+ expect(result.status).toBe(200);
154
+ expect(result.headers.get('content-type')).toBe('text/plain');
155
+ expect(await result.text()).toBe('Hello World');
156
+ });
157
+
132
158
  it('should handle object responses', async () => {
133
159
  const result = processResponse({ message: 'success' }, mockContext, mockInternalCookies, true);
134
160
 
@@ -8,6 +8,7 @@ import {
8
8
  parseRequestBody,
9
9
  cookiesToHeaders,
10
10
  mergeHeadersWithCookies,
11
+ headersToPlainObject,
11
12
  createRedirectResponse,
12
13
  isFileUpload,
13
14
  getFileFromUpload,
@@ -178,6 +179,47 @@ describe('Utility Functions', () => {
178
179
  expect(result.get('content-type')).toBe('application/json');
179
180
  expect(result.get('set-cookie')).toBe('session=abc123');
180
181
  });
182
+
183
+ it('should preserve special header casing', () => {
184
+ const headers = {
185
+ 'WWW-Authenticate': 'Basic realm="example"',
186
+ 'x-frame-options': 'DENY',
187
+ 'content-type': 'application/json'
188
+ };
189
+
190
+ const result = mergeHeadersWithCookies(headers, []);
191
+
192
+ // Check that special headers maintain their casing
193
+ expect(result.get('WWW-Authenticate')).toBe('Basic realm="example"');
194
+ expect(result.get('X-Frame-Options')).toBe('DENY');
195
+ // Regular headers should keep their original casing
196
+ expect(result.get('content-type')).toBe('application/json');
197
+ });
198
+ });
199
+
200
+ describe('headersToPlainObject', () => {
201
+ it('should convert Headers object to plain object', () => {
202
+ const headers = new Headers();
203
+ headers.set('www-authenticate', 'Basic realm="example"');
204
+ headers.set('x-frame-options', 'DENY');
205
+ headers.set('content-type', 'application/json');
206
+
207
+ const result = headersToPlainObject(headers);
208
+
209
+ // Note: Headers object normalizes keys to lowercase
210
+ expect(result).toEqual({
211
+ 'www-authenticate': 'Basic realm="example"',
212
+ 'x-frame-options': 'DENY',
213
+ 'content-type': 'application/json'
214
+ });
215
+ });
216
+
217
+ it('should handle empty Headers object', () => {
218
+ const headers = new Headers();
219
+ const result = headersToPlainObject(headers);
220
+
221
+ expect(result).toEqual({});
222
+ });
181
223
  });
182
224
 
183
225
  describe('createRedirectResponse', () => {