@@ -7,6 +7,7 @@ import fsp from "node:fs/promises";
77import path from "node:path" ;
88import os from "node:os" ;
99import zlib from "node:zlib" ;
10+ import http from "node:http" ;
1011import { StaticFileCache } from "../packages/vinext/src/server/static-file-cache.js" ;
1112import { tryServeStatic } from "../packages/vinext/src/server/prod-server.js" ;
1213import type { IncomingMessage , ServerResponse } from "node:http" ;
@@ -131,7 +132,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
131132 expect ( served ) . toBe ( true ) ;
132133 expect ( captured . headers [ "Content-Encoding" ] ) . toBe ( "br" ) ;
133134 expect ( captured . headers [ "Content-Length" ] ) . toBe ( String ( brContent . length ) ) ;
134- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript" ) ;
135+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript; charset=utf-8 " ) ;
135136 // Body should be the precompressed brotli content
136137 const decompressed = zlib . brotliDecompressSync ( captured . body ) . toString ( ) ;
137138 expect ( decompressed ) . toBe ( jsContent ) ;
@@ -480,7 +481,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
480481 await captured . ended ;
481482 expect ( served ) . toBe ( true ) ;
482483 expect ( captured . status ) . toBe ( 200 ) ;
483- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript" ) ;
484+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript; charset=utf-8 " ) ;
484485 expect ( captured . headers [ "Content-Length" ] ) . toBe ( String ( jsContent . length ) ) ;
485486 expect ( captured . body . length ) . toBe ( 0 ) ; // no body for HEAD
486487 } ) ;
@@ -729,7 +730,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
729730 await captured . ended ;
730731 expect ( served ) . toBe ( true ) ;
731732 expect ( captured . status ) . toBe ( 200 ) ;
732- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript" ) ;
733+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "application/javascript; charset=utf-8 " ) ;
733734 expect ( captured . body . toString ( ) ) . toBe ( "slow path content" ) ;
734735 } ) ;
735736
@@ -795,6 +796,63 @@ describe("tryServeStatic (with StaticFileCache)", () => {
795796 expect ( captured . body . length ) . toBe ( 0 ) ;
796797 } ) ;
797798
799+ it ( "serves Next-compatible MIME types over a real HTTP response" , async ( ) => {
800+ // Next.js uses its bundled `send` MIME database, which adds UTF-8 to
801+ // text/*, application/javascript, and application/json responses.
802+ // https://github.com/vercel/next.js/blob/canary/packages/next/src/server/serve-static.ts
803+ await Promise . all ( [
804+ writeFile ( clientDir , "script.js" , "console.log('ok')" ) ,
805+ writeFile ( clientDir , "style.css" , "body {}" ) ,
806+ writeFile ( clientDir , "data.json" , "{}" ) ,
807+ writeFile ( clientDir , "script.js.map" , "{}" ) ,
808+ writeFile ( clientDir , "table.csv" , "name,value" ) ,
809+ writeFile ( clientDir , "module.wasm" , Buffer . from ( [ 0 , 97 , 115 , 109 ] ) ) ,
810+ ] ) ;
811+
812+ for ( const cache of [ await StaticFileCache . create ( clientDir ) , undefined ] ) {
813+ const server = http . createServer ( ( req , res ) => {
814+ void tryServeStatic ( req , res , clientDir , req . url ?? "/" , false , cache )
815+ . then ( ( served ) => {
816+ if ( ! served ) {
817+ res . statusCode = 404 ;
818+ res . end ( ) ;
819+ }
820+ } )
821+ . catch ( ( error : Error ) => res . destroy ( error ) ) ;
822+ } ) ;
823+ await new Promise < void > ( ( resolve ) => server . listen ( 0 , "127.0.0.1" , resolve ) ) ;
824+
825+ try {
826+ const address = server . address ( ) ;
827+ if ( ! address || typeof address === "string" ) throw new Error ( "HTTP server did not bind" ) ;
828+ const origin = `http://127.0.0.1:${ address . port } ` ;
829+
830+ for ( const [ pathname , expected ] of [
831+ [ "/script.js" , "application/javascript; charset=utf-8" ] ,
832+ [ "/style.css" , "text/css; charset=utf-8" ] ,
833+ [ "/data.json" , "application/json; charset=utf-8" ] ,
834+ [ "/script.js.map" , "application/json; charset=utf-8" ] ,
835+ [ "/table.csv" , "text/csv; charset=utf-8" ] ,
836+ [ "/module.wasm" , "application/wasm" ] ,
837+ ] as const ) {
838+ const response = await fetch ( origin + pathname ) ;
839+ expect ( response . status ) . toBe ( 200 ) ;
840+ expect ( response . headers . get ( "content-type" ) ) . toBe ( expected ) ;
841+ }
842+
843+ const headResponse = await fetch ( origin + "/script.js" , { method : "HEAD" } ) ;
844+ expect ( headResponse . headers . get ( "content-type" ) ) . toBe (
845+ "application/javascript; charset=utf-8" ,
846+ ) ;
847+ expect ( await headResponse . text ( ) ) . toBe ( "" ) ;
848+ } finally {
849+ await new Promise < void > ( ( resolve , reject ) => {
850+ server . close ( ( error ) => ( error ? reject ( error ) : resolve ( ) ) ) ;
851+ } ) ;
852+ }
853+ }
854+ } ) ;
855+
798856 // ── URL-encoded characters in path ─────────────────────────────
799857 //
800858 // Regression test for https://github.com/cloudflare/vinext/issues/1472
@@ -829,7 +887,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
829887 await captured . ended ;
830888 expect ( served ) . toBe ( true ) ;
831889 expect ( captured . status ) . toBe ( 200 ) ;
832- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css" ) ;
890+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css; charset=utf-8 " ) ;
833891 expect ( captured . body . toString ( ) ) . toBe ( cssContent ) ;
834892 } ) ;
835893
@@ -851,7 +909,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
851909 await captured . ended ;
852910 expect ( served ) . toBe ( true ) ;
853911 expect ( captured . status ) . toBe ( 200 ) ;
854- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css" ) ;
912+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css; charset=utf-8 " ) ;
855913 expect ( captured . body . toString ( ) ) . toBe ( cssContent ) ;
856914 } ) ;
857915
@@ -880,7 +938,7 @@ describe("tryServeStatic (with StaticFileCache)", () => {
880938 await captured . ended ;
881939 expect ( served ) . toBe ( true ) ;
882940 expect ( captured . status ) . toBe ( 200 ) ;
883- expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css" ) ;
941+ expect ( captured . headers [ "Content-Type" ] ) . toBe ( "text/css; charset=utf-8 " ) ;
884942 expect ( captured . body . toString ( ) ) . toBe ( cssContent ) ;
885943 } ) ;
886944
0 commit comments