// noinspection TypeScriptValidateJSTypes import assert from "node:assert"; import childProcess from "node:child_process"; import { once } from "node:events"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; import http from "node:http"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import { json, text } from "node:stream/consumers"; import url from "node:url"; import util from "node:util"; import { _forceColour, _transformsForContentEncodingAndContentType, createFetchMock, DeferredPromise, fetch, kCurrentWorker, Miniflare, MiniflareCoreError, parseWithRootPath, PLUGINS, Response, viewToBuffer, } from "miniflare"; import { afterEach, test, vi } from "vitest"; import { WebSocketServer } from "ws"; import { assertIsV2ModuleFallbackProtocol } from "../src/plugins/core/module-fallback"; import { FIXTURES_PATH, TestLog, useCwd, useDispose, useServer, useTmp, utf8Encode, } from "./test-shared"; import type { D1Database, DurableObjectNamespace, Fetcher, KVNamespace, Queue, R2Bucket, } from "@cloudflare/workers-types/experimental"; import type { MessageEvent, MiniflareOptions, ReplaceWorkersTypes, Worker_Module, WorkerOptions, } from "miniflare"; import type { AddressInfo } from "node:net"; import type { Writable } from "node:stream"; import type { CloseEvent as StandardCloseEvent, MessageEvent as StandardMessageEvent, } from "ws"; // (base64 encoded module containing a single `add(i32, i32): i32` export) const ADD_WASM_MODULE = Buffer.from( "AGFzbQEAAAABBwFgAn9/AX8DAgEABwcBA2FkZAAACgkBBwAgACABagsACgRuYW1lAgMBAAA=", "base64" ); afterEach(() => { vi.unstubAllEnvs(); }); test("Miniflare: validates options", async ({ expect, onTestFinished }) => { // Check empty workers array rejected expect(() => new Miniflare({ workers: [] })).toThrow( new MiniflareCoreError("ERR_NO_WORKERS", "No workers defined") ); // Check workers with the same name rejected expect( () => new Miniflare({ workers: [{ script: "" }, { script: "" }], }) ).toThrow( new MiniflareCoreError( "ERR_DUPLICATE_NAME", "Multiple workers defined without a `name`" ) ); expect( () => new Miniflare({ workers: [ { script: "" }, { script: "", name: "a" }, { script: "", name: "b" }, { script: "", name: "a" }, ], }) ).toThrow( new MiniflareCoreError( "ERR_DUPLICATE_NAME", 'Multiple workers defined with the same `name`: "a"' ) ); // Disable colours for easier to read expectations _forceColour(false); onTestFinished(() => _forceColour()); // Check throws validation error with incorrect options let error: MiniflareCoreError | undefined = undefined; try { // @ts-expect-error intentionally testing incorrect types new Miniflare({ name: 42, script: "" }); } catch (e) { error = e as MiniflareCoreError; } expect(error).toBeInstanceOf(MiniflareCoreError); expect(error?.code).toEqual("ERR_VALIDATION"); expect(error?.message).toEqual( `Unexpected options passed to \`new Miniflare()\` constructor: { name: 42, ^ Expected string, received number ..., }` ); // Check throws validation error with primitive option error = undefined; try { // @ts-expect-error intentionally testing incorrect types new Miniflare("addEventListener(...)"); } catch (e) { error = e as MiniflareCoreError; } expect(error).toBeInstanceOf(MiniflareCoreError); expect(error?.code).toEqual("ERR_VALIDATION"); expect(error?.message).toEqual( `Unexpected options passed to \`new Miniflare()\` constructor: 'addEventListener(...)' ^ Expected object, received string` ); }); test("Miniflare: accepts mixed r2Buckets record", () => { const mf = new Miniflare({ modules: true, script: "", r2Buckets: { LOCAL_BUCKET: "local-bucket", REMOTE_BUCKET: { id: "remote-bucket" }, }, }); useDispose(mf); }); test("Miniflare: accepts mixed kvNamespaces record", () => { const mf = new Miniflare({ modules: true, script: "", kvNamespaces: { LOCAL_NS: "local-ns", REMOTE_NS: { id: "remote-ns" }, }, }); useDispose(mf); }); test("Miniflare: accepts mixed d1Databases record", () => { const mf = new Miniflare({ modules: true, script: "", d1Databases: { LOCAL_DB: "local-db", REMOTE_DB: { id: "remote-db" }, }, }); useDispose(mf); }); test("Miniflare: accepts mixed pipelines record", () => { const mf = new Miniflare({ modules: true, script: "", pipelines: { LOCAL_PIPELINE: "local-pipeline", REMOTE_PIPELINE: { pipeline: "remote-pipeline" }, }, }); useDispose(mf); }); test("Miniflare: ready returns copy of entry URL", async ({ expect }) => { const mf = new Miniflare({ port: 0, modules: true, script: "", }); useDispose(mf); const url1 = await mf.ready; url1.protocol = "ws:"; const url2 = await mf.ready; expect(url1).not.toBe(url2); expect(url2.protocol).toBe("http:"); }); test("Miniflare: setOptions: can update host/port", async ({ expect }) => { // Extract loopback port from injected live reload script const loopbackPortRegexp = /\/\/ Miniflare Live Reload.+url\.port = (\d+)/s; const opts: MiniflareOptions = { port: 0, inspectorPort: 0, liveReload: true, script: `addEventListener("fetch", (event) => { event.respondWith(new Response("

👋

", { headers: { "Content-Type": "text/html;charset=utf-8" } })); })`, }; const mf = new Miniflare(opts); useDispose(mf); async function getState() { const url = await mf.ready; const inspectorUrl = await mf.getInspectorURL(); const res = await mf.dispatchFetch("http://localhost"); const loopbackPort = loopbackPortRegexp.exec(await res.text())?.[1]; return { url, inspectorUrl, loopbackPort }; } const state1 = await getState(); opts.host = "0.0.0.0"; await mf.setOptions(opts); const state2 = await getState(); // Make sure ports were reused when `port: 0` passed to `setOptions()` expect(state1.url.port).not.toBe("0"); expect(state1.url.port).toBe(state2.url.port); expect(state1.inspectorUrl.port).not.toBe("0"); expect(state1.inspectorUrl.port).toBe(state2.inspectorUrl.port); // Make sure updating the host restarted the loopback server expect(state1.loopbackPort).toBeDefined(); expect(state2.loopbackPort).toBeDefined(); expect(state1.loopbackPort).not.toBe(state2.loopbackPort); // Make sure setting port to `undefined` always gives a new port, but keeps // existing loopback server opts.port = undefined; await mf.setOptions(opts); const state3 = await getState(); expect(state3.url.port).not.toBe("0"); expect(state1.url.port).not.toBe(state3.url.port); expect(state2.loopbackPort).toBe(state3.loopbackPort); }); const interfaces = os.networkInterfaces(); const localInterface = (interfaces["en0"] ?? interfaces["eth0"])?.find( ({ family }) => family === "IPv4" ); (localInterface === undefined ? test.skip : test)( "Miniflare: can use local network address as host", async ({ expect }) => { assert(localInterface !== undefined); const mf = new Miniflare({ host: localInterface.address, modules: true, script: `export default { fetch(request, env) { return env.SERVICE.fetch(request); } }`, serviceBindings: { SERVICE() { return new Response("body"); }, }, }); useDispose(mf); let res = await mf.dispatchFetch("https://example.com"); expect(await res.text()).toBe("body"); const worker = await mf.getWorker(); res = await worker.fetch("https://example.com"); expect(await res.text()).toBe("body"); } ); test("Miniflare: can use localhost as host", async ({ expect }) => { const mf = new Miniflare({ host: "localhost", modules: true, script: `export default { fetch(request, env) { return env.SERVICE.fetch(request); } }`, serviceBindings: { SERVICE() { return new Response("body"); }, }, }); useDispose(mf); const url = await mf.ready; expect(url.hostname).toBe("localhost"); let res = await mf.dispatchFetch("https://example.com"); expect(await res.text()).toBe("body"); const worker = await mf.getWorker(); res = await worker.fetch("https://example.com"); expect(await res.text()).toBe("body"); }); test("Miniflare: can use IPv6 loopback as host", async ({ expect }) => { const mf = new Miniflare({ host: "::1", modules: true, script: `export default { fetch(request, env) { return env.SERVICE.fetch(request); } }`, serviceBindings: { SERVICE() { return new Response("body"); }, }, }); useDispose(mf); let res = await mf.dispatchFetch("https://example.com"); expect(await res.text()).toBe("body"); const worker = await mf.getWorker(); res = await worker.fetch("https://example.com"); expect(await res.text()).toBe("body"); }); test("Miniflare: routes to multiple workers with fallback", async ({ expect, }) => { const opts: MiniflareOptions = { workers: [ { name: "a", routes: ["*/api"], script: `addEventListener("fetch", (event) => { event.respondWith(new Response("a")); })`, }, { name: "b", routes: ["*/api/*"], // Less specific than "a"'s script: `addEventListener("fetch", (event) => { event.respondWith(new Response("b")); })`, }, ], }; const mf = new Miniflare(opts); useDispose(mf); // Check "a"'s more specific route checked first let res = await mf.dispatchFetch("http://localhost/api"); expect(await res.text()).toBe("a"); // Check "b" still accessible res = await mf.dispatchFetch("http://localhost/api/2"); expect(await res.text()).toBe("b"); // Check fallback to first res = await mf.dispatchFetch("http://localhost/notapi"); expect(await res.text()).toBe("a"); }); test("Miniflare: custom service using Content-Encoding header", async ({ expect, }) => { const testBody = "x".repeat(100); const { http } = await useServer((req, res) => { const testEncoding = req.headers["x-test-encoding"]?.toString(); const contentType = "text/html"; // known content-type that will always be compressed const encoders = _transformsForContentEncodingAndContentType( testEncoding, contentType ); let initialStream: Writable = res; for (let i = encoders.length - 1; i >= 0; i--) { encoders[i].pipe(initialStream); initialStream = encoders[i]; } res.writeHead(200, { "Content-Encoding": testEncoding, "Content-Type": contentType, }); initialStream.write(testBody); initialStream.end(); }); const mf = new Miniflare({ compatibilityFlags: ["brotli_content_encoding"], script: `addEventListener("fetch", (event) => { event.respondWith(CUSTOM.fetch(event.request)); })`, serviceBindings: { CUSTOM(request) { return fetch(http, request); }, }, }); useDispose(mf); const testEncoding = async (encoding: string) => { const res = await mf.dispatchFetch("http://localhost", { headers: { "Accept-Encoding": encoding, "X-Test-Encoding": encoding, }, }); expect(await res.text()).toBe(testBody); // This header is mostly just for this test -- but is an indication for anyone who wants to know if the response _was_ compressed // Expected the response, before decoding, to be encoded as ${encoding} expect(res.headers.get("MF-Content-Encoding")).toBe(encoding); // Ensure this header has been removed -- undici.fetch has already decoded (decompressed) the response // Expected Content-Encoding header to be removed expect(res.headers.get("Content-Encoding")).toBe(null); }; await testEncoding("gzip"); await testEncoding("deflate"); await testEncoding("br"); // `undici`'s `fetch()` is currently broken when `Content-Encoding` specifies // multiple encodings. Once https://github.com/nodejs/undici/pull/2159 is // released, we can re-enable this test. // TODO(soon): re-enable this test // await test("deflate, gzip"); }); test("Miniflare: negotiates acceptable encoding", async ({ expect }) => { const testBody = "x".repeat(100); const mf = new Miniflare({ bindings: { TEST_BODY: testBody }, compatibilityFlags: ["brotli_content_encoding"], modules: true, script: ` export default { async fetch(request, env, ctx) { const url = new URL(request.url); switch (url.pathname) { case "/": { return Response.json({ AcceptEncoding: request.headers.get("Accept-Encoding"), clientAcceptEncoding: request.cf.clientAcceptEncoding, }); } // Content-Encoding routes case "/gzip": { return new Response(env.TEST_BODY, { headers: { "Content-Encoding": "gzip", } }); } case "/br": { return new Response(env.TEST_BODY, { headers: { "Content-Encoding": "br", } }); } case "/deflate": { // workerd doesn't automatically encode "deflate" const response = new Response(env.TEST_BODY); const compressionStream = new CompressionStream("deflate"); const compressedBody = response.body.pipeThrough(compressionStream); return new Response(compressedBody, { encodeBody: "manual", headers: { "Content-Encoding": "deflate", }, }); } // Content-Type routes case "/default-compressed": { return new Response(env.TEST_BODY, { headers: { "Content-Type": "text/html", } }); } case "/default-uncompressed": { return new Response(env.TEST_BODY, { headers: { "Content-Type": "text/event-stream", } }); } default: { return new Response(null, { status: 404 }); } } }, }; `, }); useDispose(mf); // Using `fetch()` directly to simulate eyeball const url = await mf.ready; const gzipUrl = new URL("/gzip", url); const brUrl = new URL("/br", url); const deflateUrl = new URL("/deflate", url); const defaultCompressedUrl = new URL("/default-compressed", url); const defaultUncompressedUrl = new URL("/default-uncompressed", url); // https://github.com/cloudflare/workers-sdk/issues/5246 let res = await fetch(url, { headers: { "Accept-Encoding": "hello" }, }); expect(await res.json()).toEqual({ AcceptEncoding: "br, gzip", clientAcceptEncoding: "hello", }); // Check all encodings supported res = await fetch(gzipUrl); expect(await res.text()).toBe(testBody); res = await fetch(brUrl); expect(await res.text()).toBe(testBody); res = await fetch(deflateUrl); expect(await res.text()).toBe(testBody); // Check with `Accept-Encoding: gzip` res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "gzip" } }); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); res = await fetch(brUrl, { headers: { "Accept-Encoding": "gzip" } }); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); // "deflate" isn't an accepted encoding inside Workers, so returned as is res = await fetch(deflateUrl, { headers: { "Accept-Encoding": "gzip" } }); expect(res.headers.get("Content-Encoding")).toBe("deflate"); expect(await res.text()).toBe(testBody); // Check with `Accept-Encoding: br` res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "br" } }); expect(res.headers.get("Content-Encoding")).toBe("br"); expect(await res.text()).toBe(testBody); res = await fetch(brUrl, { headers: { "Accept-Encoding": "br" } }); expect(res.headers.get("Content-Encoding")).toBe("br"); expect(await res.text()).toBe(testBody); // "deflate" isn't an accepted encoding inside Workers, so returned as is res = await fetch(deflateUrl, { headers: { "Accept-Encoding": "br" } }); expect(res.headers.get("Content-Encoding")).toBe("deflate"); expect(await res.text()).toBe(testBody); // Check with mixed `Accept-Encoding` res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "gzip, br" } }); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "br, gzip" } }); expect(res.headers.get("Content-Encoding")).toBe("br"); expect(await res.text()).toBe(testBody); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "br;q=0.5, gzip" }, }); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); // Check empty `Accept-Encoding` res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "" } }); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); // Check identity encoding res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "identity" } }); expect(res.headers.get("Content-Encoding")).toBe(null); expect(await res.text()).toBe(testBody); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "*" } }); expect(res.headers.get("Content-Encoding")).toBe(null); expect(await res.text()).toBe(testBody); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "zstd, *" } }); expect(res.headers.get("Content-Encoding")).toBe(null); expect(await res.text()).toBe(testBody); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "zstd, identity;q=0" }, }); expect(res.status).toBe(415); expect(res.headers.get("Accept-Encoding")).toBe("br, gzip"); expect(await res.text()).toBe("Unsupported Media Type"); res = await fetch(gzipUrl, { headers: { "Accept-Encoding": "zstd, *;q=0" } }); expect(res.status).toBe(415); expect(res.headers.get("Accept-Encoding")).toBe("br, gzip"); expect(await res.text()).toBe("Unsupported Media Type"); // Check malformed `Accept-Encoding` res = await fetch(gzipUrl, { headers: { "Accept-Encoding": ",(,br,,,q=," } }); expect(res.headers.get("Content-Encoding")).toBe("br"); expect(await res.text()).toBe(testBody); // Check `Content-Type: text/html` is compressed (FL always compresses html) res = await fetch(defaultCompressedUrl); expect(res.headers.get("Content-Type")).toBe("text/html"); expect(res.headers.get("Content-Encoding")).toBe("gzip"); expect(await res.text()).toBe(testBody); // Check `Content-Type: text/event-stream` is not compressed (FL does not compress this mime type) res = await fetch(defaultUncompressedUrl); expect(res.headers.get("Content-Type")).toBe("text/event-stream"); expect(res.headers.get("Content-Encoding")).toBe(null); expect(await res.text()).toBe(testBody); }); test("Miniflare: custom service using Set-Cookie header", async ({ expect, }) => { const testCookies = [ "key1=value1; Max-Age=3600", "key2=value2; Domain=example.com; Secure", ]; const { http } = await useServer((req, res) => { res.writeHead(200, { "Set-Cookie": testCookies }); res.end(); }); const mf = new Miniflare({ modules: true, script: `export default { async fetch(request, env, ctx) { const res = await env.CUSTOM.fetch(request); return Response.json(res.headers.getSetCookie()); } }`, serviceBindings: { CUSTOM(request) { return fetch(http, request); }, }, // Enable `Headers#getSetCookie()`: // https://github.com/cloudflare/workerd/blob/14b54764609c263ea36ab862bb8bf512f9b1387b/src/workerd/io/compatibility-date.capnp#L273-L278 compatibilityDate: "2023-03-01", }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.json()).toEqual(testCookies); }); test("Miniflare: web socket kitchen sink", async ({ expect, onTestFinished, }) => { // Create deferred promises for asserting asynchronous event results const clientEventPromise = new DeferredPromise(); const serverMessageEventPromise = new DeferredPromise(); const serverCloseEventPromise = new DeferredPromise(); // Create WebSocket origin server const server = http.createServer(); const wss = new WebSocketServer({ server, handleProtocols(protocols) { expect(protocols).toEqual(new Set(["protocol1", "protocol2"])); return "protocol2"; }, }); wss.on("connection", (ws, req) => { // Testing receiving additional headers sent from upgrade request expect(req.headers["user-agent"]).toBe("Test"); ws.send("hello from server"); ws.addEventListener("message", serverMessageEventPromise.resolve); ws.addEventListener("close", serverCloseEventPromise.resolve); }); wss.on("headers", (headers) => { headers.push("Set-Cookie: key=value"); }); const port = await new Promise((resolve) => { server.listen(0, () => { onTestFinished(() => { server.close(); }); resolve((server.address() as AddressInfo).port); }); }); // Create Miniflare instance with WebSocket worker and custom service binding // fetching from WebSocket origin server const mf = new Miniflare({ script: `addEventListener("fetch", (event) => { event.respondWith(CUSTOM.fetch(event.request)); })`, serviceBindings: { // Testing loopback server WebSocket coupling CUSTOM(request) { // Testing dispatchFetch custom cf injection expect(request.cf?.country).toBe("MF"); // Testing dispatchFetch injects default cf values expect(request.cf?.regionCode).toBe("TX"); expect(request.headers.get("MF-Custom-Service")).toBe(null); // Testing WebSocket-upgrading fetch return fetch(`http://localhost:${port}`, request); }, }, }); useDispose(mf); // Testing dispatchFetch WebSocket coupling const res = await mf.dispatchFetch("http://localhost", { headers: { Upgrade: "websocket", "User-Agent": "Test", "Sec-WebSocket-Protocol": "protocol1, protocol2", }, cf: { country: "MF" }, }); assert(res.webSocket); res.webSocket.addEventListener("message", clientEventPromise.resolve); res.webSocket.accept(); res.webSocket.send("hello from client"); res.webSocket.close(1000, "Test Closure"); // Test receiving additional headers from upgrade response expect(res.headers.get("Set-Cookie")).toBe("key=value"); expect(res.headers.get("Sec-WebSocket-Protocol")).toBe("protocol2"); // Check event results const clientEvent = await clientEventPromise; const serverMessageEvent = await serverMessageEventPromise; const serverCloseEvent = await serverCloseEventPromise; expect(clientEvent.data).toBe("hello from server"); expect(serverMessageEvent.data).toBe("hello from client"); expect(serverCloseEvent.code).toBe(1000); expect(serverCloseEvent.reason).toBe("Test Closure"); }); test("Miniflare: custom service binding to another Miniflare instance", async ({ expect, }) => { const mfOther = new Miniflare({ modules: true, script: `export default { async fetch(request) { const { method, url } = request; const body = request.body && await request.text(); return Response.json({ method, url, body }); } }`, }); useDispose(mfOther); const mf = new Miniflare({ script: `addEventListener("fetch", (event) => { event.respondWith(CUSTOM.fetch(event.request)); })`, serviceBindings: { async CUSTOM(request) { // Check internal keys removed (e.g. `MF-Custom-Service`, `MF-Original-URL`) // https://github.com/cloudflare/miniflare/issues/475 const keys = [...request.headers.keys()]; expect( keys.filter((key) => key.toLowerCase().startsWith("mf-")) ).toEqual([]); return await mfOther.dispatchFetch(request); }, }, }); useDispose(mf); // Checking URL (including protocol/host) and body preserved through // `dispatchFetch()` and custom service bindings let res = await mf.dispatchFetch("https://custom1.mf/a?key=value"); expect(await res.json()).toEqual({ method: "GET", url: "https://custom1.mf/a?key=value", body: null, }); res = await mf.dispatchFetch("https://custom2.mf/b", { method: "POST", body: "body", }); expect(await res.json()).toEqual({ method: "POST", url: "https://custom2.mf/b", body: "body", }); // https://github.com/cloudflare/miniflare/issues/476 res = await mf.dispatchFetch("https://custom3.mf/c", { method: "DELETE" }); expect(await res.json()).toEqual({ method: "DELETE", url: "https://custom3.mf/c", body: null, }); }); test("Miniflare: service binding to current worker", async ({ expect }) => { const mf = new Miniflare({ serviceBindings: { SELF: kCurrentWorker }, modules: true, script: `export default { async fetch(request, env) { const { pathname } = new URL(request.url); if (pathname === "/callback") return new Response("callback"); const response = await env.SELF.fetch("http://placeholder/callback"); const text = await response.text(); return new Response("body:" + text); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("body:callback"); }); test("Miniflare: service binding to network", async ({ expect }) => { const { http } = await useServer((req, res) => res.end("network")); const mf = new Miniflare({ serviceBindings: { NETWORK: { network: { allow: ["private"] } } }, modules: true, script: `export default { fetch(request, env) { return env.NETWORK.fetch(request); } }`, }); useDispose(mf); const res = await mf.dispatchFetch(http); expect(await res.text()).toBe("network"); }); test("Miniflare: service binding to external server", async ({ expect }) => { const { http } = await useServer((req, res) => res.end("external")); const mf = new Miniflare({ serviceBindings: { EXTERNAL: { external: { address: http.host, http: {} } }, }, modules: true, script: `export default { fetch(request, env) { return env.EXTERNAL.fetch(request); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://example.com"); expect(await res.text()).toBe("external"); }); test("Miniflare: service binding to disk", async ({ expect }) => { const tmp = await useTmp(); const testPath = path.join(tmp, "test.txt"); await fs.writeFile(testPath, "👋"); const mf = new Miniflare({ serviceBindings: { DISK: { disk: { path: tmp, writable: true } }, }, modules: true, script: `export default { fetch(request, env) { return env.DISK.fetch(request); } }`, }); useDispose(mf); let res = await mf.dispatchFetch("https://example.com/test.txt"); expect(await res.text()).toBe("👋"); res = await mf.dispatchFetch("https://example.com/test.txt", { method: "PUT", body: "✏️", }); expect(res.status).toBe(204); expect(await fs.readFile(testPath, "utf8")).toBe("✏️"); }); test("Miniflare: service binding to named entrypoint", async ({ expect }) => { const mf = new Miniflare({ workers: [ { name: "a", serviceBindings: { A_RPC_SERVICE: { name: kCurrentWorker, entrypoint: "RpcEntrypoint" }, A_NAMED_SERVICE: { name: "a", entrypoint: "namedEntrypoint" }, B_NAMED_SERVICE: { name: "b", entrypoint: "anotherNamedEntrypoint" }, }, compatibilityFlags: ["rpc"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class RpcEntrypoint extends WorkerEntrypoint { ping() { return "a:rpc:pong"; } } export const namedEntrypoint = { fetch(request, env, ctx) { return new Response("a:named:pong"); } }; export default { async fetch(request, env) { const aRpc = await env.A_RPC_SERVICE.ping(); const aNamed = await (await env.A_NAMED_SERVICE.fetch("http://placeholder")).text(); const bNamed = await (await env.B_NAMED_SERVICE.fetch("http://placeholder")).text(); return Response.json({ aRpc, aNamed, bNamed }); } } `, }, { name: "b", modules: true, script: ` export const anotherNamedEntrypoint = { fetch(request, env, ctx) { return new Response("b:named:pong"); } }; `, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://placeholder"); expect(await res.json()).toEqual({ aRpc: "a:rpc:pong", aNamed: "a:named:pong", bNamed: "b:named:pong", }); }); test("Miniflare: service binding to named entrypoint that implements a method returning a plain object", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { name: "a", serviceBindings: { RPC_SERVICE: { name: "b", entrypoint: "RpcEntrypoint" }, }, compatibilityFlags: ["rpc"], modules: true, script: ` export default { async fetch(request, env) { const obj = await env.RPC_SERVICE.getObject(); return Response.json({ obj }); } } `, }, { name: "b", modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class RpcEntrypoint extends WorkerEntrypoint { getObject() { return { isPlainObject: true, value: 123, } } } `, }, ], }); useDispose(mf); const bindings = await mf.getBindings<{ RPC_SERVICE: any }>(); const o = await bindings.RPC_SERVICE.getObject(); expect(o.isPlainObject).toEqual(true); expect(o.value).toEqual(123); }); test("Miniflare: service binding to named entrypoint that implements a method returning an RpcTarget instance", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { name: "a", serviceBindings: { RPC_SERVICE: { name: "b", entrypoint: "RpcEntrypoint" }, }, compatibilityFlags: ["rpc"], modules: true, script: ` export default { async fetch(request, env) { const rpcTarget = await env.RPC_SERVICE.getRpcTarget(); return Response.json(rpcTarget.id); } } `, }, { name: "b", modules: true, script: ` import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; export class RpcEntrypoint extends WorkerEntrypoint { getRpcTarget() { return new SubService("test-id"); } } class SubService extends RpcTarget { #id constructor(id) { super() this.#id = id } get id() { return this.#id } } `, }, ], }); useDispose(mf); const bindings = await mf.getBindings<{ RPC_SERVICE: any }>(); const rpcTarget = await bindings.RPC_SERVICE.getRpcTarget(); expect(rpcTarget.id).toEqual("test-id"); }); test("Miniflare: tail consumer called", async ({ expect }) => { const mf = new Miniflare({ handleRuntimeStdio: () => {}, workers: [ { name: "a", tails: ["b"], compatibilityDate: "2025-04-28", modules: true, script: ` export default { async fetch(request, env) { if(request.url.includes("b")) { return env.B.fetch(request)} console.log("Tail: log event") return new Response("hello from a"); } } `, serviceBindings: { B: "b", }, }, { name: "b", modules: true, compatibilityDate: "2025-04-28", script: ` let event; export default { fetch() {return Response.json(event)}, tail(e) {event = e } }; `, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://placeholder"); expect(await res.text()).toEqual("hello from a"); expect( ( (await (await mf.dispatchFetch("http://placeholder/b")).json()) as { logs: { message: string[] }[]; }[] )[0].logs[0].message ).toEqual(["Tail: log event"]); }); test("Miniflare: custom outbound service", async ({ expect }) => { const mf = new Miniflare({ workers: [ { name: "a", modules: true, script: `export default { async fetch() { const res1 = await (await fetch("https://example.com/1")).text(); const res2 = await (await fetch("https://example.com/2")).text(); return Response.json({ res1, res2 }); } }`, outboundService: "b", }, { name: "b", modules: true, script: `export default { async fetch(request, env) { if (request.url === "https://example.com/1") { return new Response("one"); } else { return fetch(request); } } }`, outboundService(request) { return new Response(`fallback:${request.url}`); }, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.json()).toEqual({ res1: "one", res2: "fallback:https://example.com/2", }); }); test("Miniflare: custom outbound service passes through TCP sockets", async ({ expect, }) => { const tcpServer = net.createServer((socket) => { socket.on("data", () => { socket.write("pong"); }); }); const listeningPromise = once(tcpServer, "listening"); tcpServer.listen(0, "127.0.0.1"); await listeningPromise; const address = tcpServer.address(); assert(typeof address === "object" && address !== null); const mf = new Miniflare({ modules: true, compatibilityDate: "2026-05-20", compatibilityFlags: ["nodejs_compat"], script: ` import { connect } from "cloudflare:sockets"; export default { async fetch() { const response = await fetch("https://example.com/path?query=1"); const fetchBody = await response.text(); const socket = connect({ hostname: "127.0.0.1", port: ${address.port} }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("ping")); const reader = socket.readable.getReader(); const { value } = await reader.read(); writer.releaseLock(); reader.releaseLock(); socket.close(); return Response.json({ fetchBody, socketBody: new TextDecoder().decode(value), }); } }; `, outboundService(request) { return new Response(`intercepted:${request.url}`); }, }); useDispose(mf); try { const response = await mf.dispatchFetch("http://localhost"); const body = await response.json(); expect({ status: response.status, body }).toEqual({ status: 200, body: { fetchBody: "intercepted:https://example.com/path?query=1", socketBody: "pong", }, }); } finally { await new Promise((resolve, reject) => { tcpServer.close((error) => (error ? reject(error) : resolve())); }); } }); test("Miniflare: can send GET request with body", async ({ expect }) => { // https://github.com/cloudflare/workerd/issues/1122 const mf = new Miniflare({ compatibilityDate: "2023-08-01", modules: true, script: `export default { async fetch(request) { return Response.json({ cf: request.cf, contentLength: request.headers.get("Content-Length"), hasBody: request.body !== null, }); } }`, cf: { key: "value" }, }); useDispose(mf); // Can't use `dispatchFetch()` here as `fetch()` prohibits `GET` requests // with bodies/`Content-Length: 0` headers const url = await mf.ready; function get(opts: http.RequestOptions = {}): Promise { return new Promise((resolve, reject) => { http.get(url, opts, resolve).on("error", reject); }); } let res = await get(); expect(await json(res)).toEqual({ cf: { key: "value", clientAcceptEncoding: "" }, contentLength: null, hasBody: false, }); res = await get({ headers: { "content-length": "0" } }); expect(await json(res)).toEqual({ cf: { key: "value", clientAcceptEncoding: "" }, contentLength: "0", hasBody: true, }); }); test("Miniflare: handles redirect responses", async ({ expect }) => { // https://github.com/cloudflare/workers-sdk/issues/5018 const { http } = await useServer((req, res) => { // Check no special headers set const headerKeys = Object.keys(req.headers); expect( headerKeys.filter((key) => key.toLowerCase().startsWith("mf-")) ).toEqual([]); const { pathname } = new URL(req.url ?? "", "http://placeholder"); if (pathname === "/ping") { res.end("pong"); } else if (pathname === "/redirect-back") { res.writeHead(302, { Location: "https://custom.mf/external-redirected" }); res.end(); } else { res.writeHead(404); res.end("Not Found"); } }); const mf = new Miniflare({ bindings: { EXTERNAL_URL: http.href }, compatibilityDate: "2024-01-01", modules: true, script: `export default { async fetch(request, env) { const url = new URL(request.url); const externalUrl = new URL(env.EXTERNAL_URL); if (url.pathname === "/redirect-relative") { return new Response(null, { status: 302, headers: { Location: "/relative-redirected" } }); } else if (url.pathname === "/redirect-absolute") { url.pathname = "/absolute-redirected"; return Response.redirect(url, 302); } else if (url.pathname === "/redirect-external") { externalUrl.pathname = "/ping"; return Response.redirect(externalUrl, 302); } else if (url.pathname === "/redirect-external-and-back") { externalUrl.pathname = "/redirect-back"; return Response.redirect(externalUrl, 302); } else { return new Response("end:" + url.href); } } }`, }); useDispose(mf); // Check relative redirect let res = await mf.dispatchFetch("https://custom.mf/redirect-relative", { redirect: "manual", }); expect(res.status).toBe(302); expect(res.headers.get("Location")).toBe("/relative-redirected"); await res.arrayBuffer(); // (drain) res = await mf.dispatchFetch("https://custom.mf/redirect-relative"); expect(res.status).toBe(200); expect(await res.text()).toBe("end:https://custom.mf/relative-redirected"); // Check absolute redirect to same origin res = await mf.dispatchFetch("https://custom.mf/redirect-absolute", { redirect: "manual", }); expect(res.status).toBe(302); expect(res.headers.get("Location")).toBe( "https://custom.mf/absolute-redirected" ); await res.arrayBuffer(); // (drain) res = await mf.dispatchFetch("https://custom.mf/redirect-absolute"); expect(res.status).toBe(200); expect(await res.text()).toBe("end:https://custom.mf/absolute-redirected"); // Check absolute redirect to external origin res = await mf.dispatchFetch("https://custom.mf/redirect-external", { redirect: "manual", }); expect(res.status).toBe(302); expect(res.headers.get("Location")).toBe(new URL("/ping", http).href); await res.arrayBuffer(); // (drain) res = await mf.dispatchFetch("https://custom.mf/redirect-external"); expect(res.status).toBe(200); expect(await res.text()).toBe("pong"); // Check absolute redirect to external origin, then redirect back to initial res = await mf.dispatchFetch("https://custom.mf/redirect-external-and-back", { redirect: "manual", }); expect(res.status).toBe(302); expect(res.headers.get("Location")).toBe( new URL("/redirect-back", http).href ); await res.arrayBuffer(); // (drain) res = await mf.dispatchFetch("https://custom.mf/redirect-external-and-back"); expect(res.status).toBe(200); // External server redirects back to worker running in `workerd` expect(await res.text()).toBe("end:https://custom.mf/external-redirected"); }); test("Miniflare: fetch mocking", async ({ expect }) => { const fetchMock = createFetchMock(); fetchMock.disableNetConnect(); const origin = fetchMock.get("https://example.com"); origin.intercept({ method: "GET", path: "/" }).reply(200, "Mocked response!"); const mfOptions: MiniflareOptions = { modules: true, script: `export default { async fetch() { return fetch("https://example.com/"); } }`, fetchMock, }; const resultOptions = {} as MiniflareOptions; // Verify that options with `fetchMock` can be parsed first before passing to Miniflare // Regression test for https://github.com/cloudflare/workers-sdk/issues/5486 for (const plugin of Object.values(PLUGINS)) { Object.assign( resultOptions, parseWithRootPath("", plugin.options, mfOptions) ); } const mf = new Miniflare(resultOptions); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("Mocked response!"); // Check `outboundService`and `fetchMock` mutually exclusive await expect( mf.setOptions({ script: "", fetchMock, outboundService: "", }) ).rejects.toThrow( new MiniflareCoreError( "ERR_MULTIPLE_OUTBOUNDS", "Only one of `outboundService` or `fetchMock` may be specified per worker" ) ); }); test("Miniflare: custom upstream as origin (with colons)", async ({ expect, }) => { const upstream = await useServer((req, res) => { res.end(`upstream: ${new URL(req.url ?? "", "http://upstream")}`); }); const mf = new Miniflare({ upstream: new URL("/extra:extra/", upstream.http.toString()).toString(), modules: true, script: `export default { fetch(request) { return fetch(request); } }`, }); useDispose(mf); // Check rewrites protocol, hostname, and port, but keeps pathname and query const res = await mf.dispatchFetch("https://random:0/path:path?a=1"); expect(await res.text()).toBe( "upstream: http://upstream/extra:extra/path:path?a=1" ); }); test("Miniflare: custom upstream as origin", async ({ expect }) => { const upstream = await useServer((req, res) => { res.end(`upstream: ${new URL(req.url ?? "", "http://upstream")}`); }); const mf = new Miniflare({ upstream: new URL("/extra/", upstream.http.toString()).toString(), modules: true, script: `export default { async fetch(request) { const resp = await (await fetch(request)).text(); return Response.json({ resp, host: request.headers.get("Host") }); } }`, }); useDispose(mf); // Check rewrites protocol, hostname, and port, but keeps pathname and query const res = await mf.dispatchFetch("https://random:0/path?a=1"); expect(await res.json()).toEqual({ resp: "upstream: http://upstream/extra/path?a=1", host: upstream.http.host, }); }); test("Miniflare: custom upstream sets MF-Original-Hostname header", async ({ expect, }) => { const upstream = await useServer((req, res) => { res.end(`upstream`); }); const mf = new Miniflare({ upstream: upstream.http.toString(), modules: true, script: `export default { async fetch(request) { return Response.json({ host: request.headers.get("Host"), originalHostname: request.headers.get("MF-Original-Hostname") }); } }`, }); useDispose(mf); // Check that original hostname is preserved when using upstream const res = await mf.dispatchFetch( "https://my-original-host.example.com:8080/path?a=1" ); expect(await res.json()).toEqual({ host: upstream.http.host, originalHostname: "my-original-host.example.com:8080", }); }); test("Miniflare: MF-Original-Hostname header not set without upstream", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: `export default { async fetch(request) { return Response.json({ originalHostname: request.headers.get("MF-Original-Hostname") }); } }`, }); useDispose(mf); // Check that original hostname header is not set when not using upstream const res = await mf.dispatchFetch("https://random:0/path?a=1"); expect(await res.json()).toEqual({ originalHostname: null, }); }); test("Miniflare: set origin to original URL if proxy shared secret matches", async ({ expect, }) => { const mf = new Miniflare({ unsafeProxySharedSecret: "SOME_PROXY_SHARED_SECRET_VALUE", modules: true, script: `export default { async fetch(request) { return Response.json({ host: request.headers.get("Host") }); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://random:0/path?a=1", { headers: { "MF-Proxy-Shared-Secret": "SOME_PROXY_SHARED_SECRET_VALUE" }, }); expect(await res.json()).toEqual({ host: "random:0", }); }); test("Miniflare: keep origin as listening host if proxy shared secret not provided", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: `export default { async fetch(request) { return Response.json({ host: request.headers.get("Host") }); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://random:0/path?a=1"); expect(await res.json()).toEqual({ host: (await mf.ready).host, }); }); test("Miniflare: 400 error on proxy shared secret header when not configured", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: `export default { async fetch(request) { return Response.json({ host: request.headers.get("Host") }); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://random:0/path?a=1", { headers: { "MF-Proxy-Shared-Secret": "SOME_PROXY_SHARED_SECRET_VALUE" }, }); expect(res.status).toBe(400); expect(await res.text()).toBe( "Disallowed header in request: MF-Proxy-Shared-Secret=SOME_PROXY_SHARED_SECRET_VALUE" ); }); test("Miniflare: 400 error on proxy shared secret header mismatch with configuration", async ({ expect, }) => { const mf = new Miniflare({ unsafeProxySharedSecret: "SOME_PROXY_SHARED_SECRET_VALUE", modules: true, script: `export default { async fetch(request) { return Response.json({ host: request.headers.get("Host") }); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://random:0/path?a=1", { headers: { "MF-Proxy-Shared-Secret": "BAD_PROXY_SHARED_SECRET" }, }); expect(res.status).toBe(400); expect(await res.text()).toBe( "Disallowed header in request: MF-Proxy-Shared-Secret=BAD_PROXY_SHARED_SECRET" ); }); test("Miniflare: `node:`, `cloudflare:` and `workerd:` modules", async ({ expect, }) => { const mf = new Miniflare({ modules: true, compatibilityFlags: ["nodejs_compat", "rtti_api"], scriptPath: "index.mjs", script: ` import assert from "node:assert"; import { Buffer } from "node:buffer"; import { connect } from "cloudflare:sockets"; import rtti from "workerd:rtti"; export default { fetch() { assert.strictEqual(typeof connect, "function"); assert.strictEqual(typeof rtti, "object"); return new Response(Buffer.from("test").toString("base64")) } } `, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("dGVzdA=="); }); test("Miniflare: modules in sub-directories", async ({ expect }) => { const mf = new Miniflare({ modules: [ { type: "ESModule", path: "index.js", contents: `import { b } from "./sub1/index.js"; export default { fetch() { return new Response(String(b + 3)); } }`, }, { type: "ESModule", path: "sub1/index.js", contents: `import { c } from "./sub2/index.js"; export const b = c + 20;`, }, { type: "ESModule", path: "sub1/sub2/index.js", contents: `export const c = 100;`, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("123"); }); test("Miniflare: python modules", async ({ expect }) => { const mf = new Miniflare({ modules: [ { type: "PythonModule", path: "index.py", contents: "from test_module import add; from workers import Response, WorkerEntrypoint;\nclass Default(WorkerEntrypoint):\n def fetch(self, request):\n return Response(str(add(2,2)))", }, { type: "PythonModule", path: "test_module.py", contents: `def add(a, b):\n return a + b`, }, ], compatibilityFlags: ["python_workers", "python_no_global_handlers"], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("4"); }); test("Miniflare: HTTPS fetches using browser CA certificates", async ({ expect, }) => { // example.com is intentionally maintained as a stable test endpoint by IANA. // We previously fetched https://workers.cloudflare.com/cf.json but that URL // now 301s to https://www.cloudflare.com/cf.json which 404s, so the test // was effectively asserting on an unintended 404 path rather than HTTPS CA // trust. const mf = new Miniflare({ modules: true, script: `export default { fetch() { return fetch("https://example.com/"); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(res.ok).toBe(true); await res.arrayBuffer(); // (drain) }); test("Miniflare: accepts https requests", async ({ expect }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, https: true, script: `export default { fetch() { return new Response("Hello world"); } }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://localhost"); expect(res.ok).toBe(true); await res.arrayBuffer(); // (drain) expect(log.logs[0][1].startsWith("Ready on https://")); }); // Regression test for https://github.com/cloudflare/workers-sdk/issues/9357 test("Miniflare: throws error messages that reflect the actual issue", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, https: true, script: `export default { async fetch(request, env, ctx) { Object.defineProperty("not an object", "node", ""); return new Response('Hello World!'); }, }`, }); useDispose(mf); const res = await mf.dispatchFetch("https://localhost"); expect(await res.text()).toMatch( /TypeError: Object\.defineProperty called on non-object/ ); }); test("Miniflare: manually triggered scheduled events", async ({ expect }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` let scheduledRun = false; export default { fetch() { return new Response(scheduledRun); }, scheduled(controller) { scheduledRun = true; controller.noRetry(); } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/scheduled?format=json" ); expect(await res.json()).toEqual({ outcome: "ok", noRetry: true }); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("true"); }); test("Miniflare: manually triggered scheduled events with assets", async ({ expect, }) => { const log = new TestLog(); const tmp = await useTmp(); await fs.writeFile( path.join(tmp, "foo.html"), "

asset

", "utf8" ); await fs.writeFile(path.join(tmp, "foo.md"), "asset", "utf8"); const mf = new Miniflare({ log, modules: true, script: ` let scheduledRun = false; let cron; let scheduledTime; export default { fetch() { return Response.json({ scheduledRun, cron, scheduledTime }); }, scheduled(controller, env, ctx) { scheduledRun = true; cron = controller.cron; scheduledTime = Number(controller.scheduledTime); controller.noRetry(); } }`, assets: { directory: tmp, routerConfig: { has_user_worker: true, }, }, unsafeTriggerHandlers: true, }); useDispose(mf); type ScheduledResult = { scheduledRun: boolean; cron?: string; scheduledTime?: number; }; let res = await mf.dispatchFetch("http://localhost"); let json = (await res.json()) as ScheduledResult; expect(json.scheduledRun).toBe(false); expect(json.cron).toBe(undefined); expect(json.scheduledTime).toBe(undefined); res = await mf.dispatchFetch("http://localhost/foo"); expect(res.headers.get("content-type")).toBe("text/html; charset=utf-8"); expect(await res.text()).toBe("

asset

"); res = await mf.dispatchFetch("http://localhost/foo.md"); expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); expect(await res.text()).toBe("asset"); res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch("http://localhost"); json = (await res.json()) as ScheduledResult; expect(json.scheduledRun).toBe(true); expect(json.cron).toBe(""); expect(json.scheduledTime).toBeDefined(); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" ); expect(await res.json()).toEqual({ outcome: "ok", noRetry: true, }); res = await mf.dispatchFetch("http://localhost"); json = (await res.json()) as ScheduledResult; expect(json.scheduledRun).toBe(true); expect(json.cron).toBe("0 0 0 0 0"); expect(json.scheduledTime).toBe(1234567890987); }); test("Miniflare: manually triggered email handler - valid email", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` let receivedEmail = false; export default { fetch() { return new Response(receivedEmail); }, email(emailMessage) { receivedEmail = true; } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await res.text()).toBe("Worker successfully processed email"); expect(res.status).toBe(200); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("true"); }); test("Miniflare: manually triggered email handler - setReject does not throw", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` let receivedEmail = false; export default { fetch() { return new Response(receivedEmail); }, async email(emailMessage) { await emailMessage.setReject("I just don't like this email :(") receivedEmail = true; } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await res.text()).toBe( "Worker rejected email with the following reason: I just don't like this email :(" ); expect(res.status).toBe(400); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("true"); }); test("Miniflare: manually triggered email handler - forward does not throw", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` let receivedEmail = false; export default { fetch() { return new Response(receivedEmail); }, async email(emailMessage) { await emailMessage.forward("mark.s@example.com") receivedEmail = true; } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await res.text()).toBe("Worker successfully processed email"); expect(res.status).toBe(200); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("true"); }); test("Miniflare: manually triggered email handler - invalid email, no message id", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` let receivedEmail = false; export default { fetch() { return new Response(receivedEmail); }, email(emailMessage) { receivedEmail = true; } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else MIME-Version: 1.0 Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await res.text()).toBe( "Email could not be parsed: invalid or no message id provided" ); expect(res.status).toBe(400); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); }); test("Miniflare: manually triggered email handler - reply handler works", async ({ expect, }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, script: ` import {EmailMessage} from "cloudflare:email" let receivedEmail = false; export default { fetch() { return new Response(receivedEmail); }, async email(emailMessage) { await emailMessage.reply(new EmailMessage( "someone-else@example.com", "someone@example.com", (new Response( \`From: someone else To: someone In-Reply-To: Message-ID: MIME-Version: 1.0 Content-Type: text/plain This is a random email body. \`)).body )); receivedEmail = true; } }`, unsafeTriggerHandlers: true, }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else MIME-Version: 1.0 Message-ID: Content-Type: text/plain This is a random email body. `, method: "POST", } ); expect(await res.text()).toBe("Worker successfully processed email"); expect(res.status).toBe(200); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("true"); }); test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: ` export default { fetch() { return new Response("Hello world"); } } `, unsafeTriggerHandlers: true, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/foo"); expect(await res.text()).toBe( `"/cdn-cgi/handler/foo" is not a valid handler. Did you mean to use "/cdn-cgi/handler/scheduled" or "/cdn-cgi/handler/email"?` ); expect(res.status).toBe(404); }); test("Miniflare: other /cdn-cgi/ routes", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: ` export default { fetch() { return new Response("Hello world"); } } `, unsafeTriggerHandlers: true, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost/cdn-cgi/foo"); expect(await res.text()).toBe("Hello world"); expect(res.status).toBe(200); }); test("Miniflare: blocks non-local Host headers from reaching /cdn-cgi/ routes", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: ` export default { fetch() { return new Response("Hello world"); } } `, unsafeTriggerHandlers: true, }); useDispose(mf); const url = await mf.ready; const directUrlStatus = await new Promise((resolve, reject) => { const req = http.get( `${url.origin}/cdn-cgi/foo`, { setHost: false, headers: { Host: "example.trycloudflare.com" } }, (res) => { res.resume(); resolve(res.statusCode ?? 0); } ); req.on("error", reject); }); expect(directUrlStatus).toBe(403); const originalUrlStatus = await new Promise((resolve, reject) => { const req = http.get( `${url.origin}/foo`, { setHost: false, headers: { Host: "example.trycloudflare.com", "MF-Original-URL": "http://localhost/cdn-cgi/handler/scheduled", }, }, (res) => { res.resume(); resolve(res.statusCode ?? 0); } ); req.on("error", reject); }); expect(originalUrlStatus).toBe(403); }); test("Miniflare: listens on ipv6", async ({ expect }) => { const log = new TestLog(); const mf = new Miniflare({ log, modules: true, host: "*", script: `export default { fetch() { return new Response("Hello world"); } }`, }); useDispose(mf); const url = await mf.ready; let response = await fetch(`http://localhost:${url.port}`); expect(response.ok).toBe(true); response = await fetch(`http://[::1]:${url.port}`); expect(response.ok).toBe(true); response = await fetch(`http://127.0.0.1:${url.port}`); expect(response.ok).toBe(true); }); test("Miniflare: dispose() immediately after construction", async ({ expect, }) => { const mf = new Miniflare({ script: "", modules: true }); const readyPromise = mf.ready; // Attach rejection handler BEFORE dispose() to prevent unhandled rejection const readyAssertion = expect(readyPromise).rejects.toThrow( new MiniflareCoreError("ERR_DISPOSED", "Cannot use disposed instance") ); await mf.dispose(); await readyAssertion; }); test("Miniflare: getBindings() returns all bindings", async ({ expect, onTestFinished, }) => { const tmp = await useTmp(); const blobPath = path.join(tmp, "blob.txt"); await fs.writeFile(blobPath, "blob"); const mf = new Miniflare({ modules: true, script: ` export class DurableObject {} export default { fetch() { return new Response(null, { status: 404 }); } } `, bindings: { STRING: "hello", OBJECT: { a: 1, b: { c: 2 } } }, textBlobBindings: { TEXT: blobPath }, dataBlobBindings: { DATA: blobPath }, serviceBindings: { SELF: "" }, d1Databases: ["DB"], durableObjects: { DO: "DurableObject" }, kvNamespaces: ["KV"], queueProducers: ["QUEUE"], r2Buckets: ["BUCKET"], }); let disposed = false; onTestFinished(() => { if (!disposed) return mf.dispose(); }); interface Env { STRING: string; OBJECT: unknown; TEXT: string; DATA: ArrayBuffer; SELF: ReplaceWorkersTypes; DB: D1Database; DO: ReplaceWorkersTypes; KV: ReplaceWorkersTypes; QUEUE: Queue; BUCKET: ReplaceWorkersTypes; } const bindings = await mf.getBindings(); expect(bindings).toMatchObject({ STRING: "hello", OBJECT: { a: 1, b: { c: 2 } }, TEXT: "blob", }); expect(bindings.DATA).toEqual(viewToBuffer(utf8Encode("blob"))); const opts: util.InspectOptions = { colors: false }; expect(util.inspect(bindings.SELF, opts)).toMatch(/name: 'Fetcher'/); expect(util.inspect(bindings.DB, opts)).toMatch(/name: 'D1Database'/); expect(util.inspect(bindings.DO, opts)).toMatch( /name: 'DurableObjectNamespace'/ ); expect(util.inspect(bindings.KV, opts)).toMatch(/name: 'KvNamespace'/); expect(util.inspect(bindings.QUEUE, opts)).toMatch(/name: 'WorkerQueue'/); expect(util.inspect(bindings.BUCKET, opts)).toMatch(/name: 'R2Bucket'/); // Check with WebAssembly binding (aren't supported by modules workers) const addWasmPath = path.join(tmp, "add.wasm"); await fs.writeFile(addWasmPath, ADD_WASM_MODULE); await mf.setOptions({ script: 'addEventListener("fetch", (event) => event.respondWith(new Response(null, { status: 404 })));', wasmBindings: { ADD: addWasmPath }, }); const { ADD } = await mf.getBindings<{ ADD: WebAssembly.Module }>(); const instance = new WebAssembly.Instance(ADD); assert(typeof instance.exports.add === "function"); expect((instance.exports.add as (a: number, b: number) => number)(1, 2)).toBe( 3 ); // Check bindings poisoned after dispose await mf.dispose(); disposed = true; expect(() => bindings.KV.get("key")).toThrow( new Error( "Attempted to use poisoned stub. Stubs to runtime objects must be re-created after calling `Miniflare#setOptions()` or `Miniflare#dispose()`." ) ); }); test("Miniflare: getBindings() returns wrapped bindings", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { Greeter: { scriptName: "greeter-implementation", }, }, modules: true, script: "", }, { modules: true, name: "greeter-implementation", script: ` class Greeter { sayHello(name) { return "Hello " + name; } } export default function (env) { return new Greeter(); } `, }, ], }); useDispose(mf); interface Env { Greeter: { sayHello: (str: string) => string; }; } const { Greeter } = await mf.getBindings(); const helloWorld = Greeter.sayHello("World"); expect(helloWorld).toBe("Hello World"); }); test("Miniflare: getBindings() handles wrapped bindings returning objects containing functions", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { Greeter: { scriptName: "greeter-obj-implementation", }, }, modules: true, script: "", }, { modules: true, name: "greeter-obj-implementation", script: ` export default function (env) { const objWithFunction = { greeting: "Hello", sayHello(name) { return this.greeting + ' ' + name; } }; return objWithFunction; } `, }, ], }); useDispose(mf); interface Env { Greeter: { greeting: string; sayHello: (str: string) => string; }; } const { Greeter } = await mf.getBindings(); const helloWorld = Greeter.sayHello("World"); expect(helloWorld).toBe("Hello World"); expect(Greeter.greeting).toBe("Hello"); }); test("Miniflare: getBindings() handles wrapped bindings returning objects containing nested functions", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { Greeter: { scriptName: "greeter-obj-implementation", }, }, modules: true, script: "", }, { modules: true, name: "greeter-obj-implementation", script: ` export default function (env) { const objWithFunction = { obj: { obj1: { obj2: { sayHello: (name) => "Hello " + name + " from a nested function" } } } }; return objWithFunction; } `, }, ], }); useDispose(mf); interface Env { Greeter: { obj: { obj1: { obj2: { sayHello: (str: string) => string; }; }; }; }; } const { Greeter } = await mf.getBindings(); const helloWorld = Greeter.obj.obj1.obj2.sayHello("World"); expect(helloWorld).toBe("Hello World from a nested function"); }); test("Miniflare: getBindings() handles wrapped bindings returning functions returning functions", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { GreetFactory: { scriptName: "greet-factory-obj-implementation", }, }, modules: true, script: "", }, { modules: true, name: "greet-factory-obj-implementation", script: ` export default function (env) { const factory = { getGreetFunction(name) { return (name) => { return this.greeting + ' ' + name; } }, greeting: "Salutations", }; return factory; } `, }, ], }); useDispose(mf); interface Env { GreetFactory: { greeting: string; getGreetFunction: () => (str: string) => string; }; } const { GreetFactory } = await mf.getBindings(); const greetFunction = GreetFactory.getGreetFunction(); expect(greetFunction("Esteemed World")).toBe("Salutations Esteemed World"); expect(GreetFactory.greeting).toBe("Salutations"); }); test("Miniflare: getWorker() allows dispatching events directly", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: ` let lastScheduledController; let lastQueueBatch; export default { async fetch(request, env, ctx) { const { pathname } = new URL(request.url); if (pathname === "/scheduled") { return Response.json({ scheduledTime: lastScheduledController?.scheduledTime, cron: lastScheduledController?.cron, }); } else if (pathname === "/queue") { return Response.json({ queue: lastQueueBatch.queue, messages: lastQueueBatch.messages.map((message) => ({ id: message.id, timestamp: message.timestamp.getTime(), body: message.body, bodyType: message.body.constructor.name, })), }); } else if (pathname === "/get-url") { return new Response(request.url); } else { return new Response(null, { status: 404 }); } }, async scheduled(controller, env, ctx) { lastScheduledController = controller; if (controller.cron === "* * * * *") controller.noRetry(); }, async queue(batch, env, ctx) { lastQueueBatch = batch; if (batch.queue === "needy") batch.retryAll(); for (const message of batch.messages) { if (message.id === "perfect") message.ack(); } } }`, }); useDispose(mf); const fetcher = await mf.getWorker(); // Check `Fetcher#scheduled()` (implicitly testing `Fetcher#fetch()`) let scheduledResult = await fetcher.scheduled({ cron: "* * * * *", }); expect(scheduledResult).toEqual({ outcome: "ok", noRetry: true }); scheduledResult = await fetcher.scheduled({ scheduledTime: new Date(1000), cron: "30 * * * *", }); expect(scheduledResult).toEqual({ outcome: "ok", noRetry: false }); let res = await fetcher.fetch("http://localhost/scheduled"); const scheduledController = await res.json(); expect(scheduledController).toEqual({ scheduledTime: 1000, cron: "30 * * * *", }); // Check `Fetcher#queue()` let queueResult = await fetcher.queue("needy", [ { id: "a", timestamp: new Date(1000), body: "a", attempts: 1 }, { id: "b", timestamp: new Date(2000), body: { b: 1 }, attempts: 1 }, ]); expect(queueResult).toEqual({ outcome: "ok", ackAll: false, retryBatch: { retry: true, }, explicitAcks: [], retryMessages: [], }); queueResult = await fetcher.queue("queue", [ { id: "c", timestamp: new Date(3000), body: new Uint8Array([1, 2, 3]), attempts: 1, }, { id: "perfect", timestamp: new Date(4000), body: new Date(5000), attempts: 1, }, ]); expect(queueResult).toEqual({ outcome: "ok", ackAll: false, retryBatch: { retry: false, }, explicitAcks: ["perfect"], retryMessages: [], }); res = await fetcher.fetch("http://localhost/queue"); const queueBatch = await res.json(); expect(queueBatch).toEqual({ queue: "queue", messages: [ { id: "c", timestamp: 3000, body: { 0: 1, 1: 2, 2: 3 }, bodyType: "Uint8Array", }, { id: "perfect", timestamp: 4000, body: "1970-01-01T00:00:05.000Z", bodyType: "Date", }, ], }); // Check `Fetcher#fetch()` res = await fetcher.fetch("https://dummy:1234/get-url"); expect(await res.text()).toBe("https://dummy:1234/get-url"); }); test("Miniflare: getBindings() and friends return bindings for different workers", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { name: "a", modules: true, script: ` export class DurableObject {} export default { fetch() { return new Response("a"); } } `, d1Databases: ["DB"], durableObjects: { DO: "DurableObject" }, }, { // 2nd worker unnamed, to validate that not specifying a name when // getting bindings gives the entrypoint, not the unnamed worker script: 'addEventListener("fetch", (event) => event.respondWith(new Response("unnamed")));', kvNamespaces: ["KV"], queueProducers: ["QUEUE"], }, { name: "b", script: 'addEventListener("fetch", (event) => event.respondWith(new Response("b")));', r2Buckets: ["BUCKET"], }, ], }); useDispose(mf); // Check `getBindings()` let bindings = await mf.getBindings(); expect(Object.keys(bindings)).toEqual(["DB", "DO"]); bindings = await mf.getBindings(""); expect(Object.keys(bindings)).toEqual(["KV", "QUEUE"]); bindings = await mf.getBindings("b"); expect(Object.keys(bindings)).toEqual(["BUCKET"]); await expect(() => mf.getBindings("c")).rejects.toThrow( new TypeError('"c" worker not found') ); // Check `getWorker()` let fetcher = await mf.getWorker(); expect(await (await fetcher.fetch("http://localhost")).text()).toBe("a"); fetcher = await mf.getWorker(""); expect(await (await fetcher.fetch("http://localhost")).text()).toBe( "unnamed" ); fetcher = await mf.getWorker("b"); expect(await (await fetcher.fetch("http://localhost")).text()).toBe("b"); await expect(() => mf.getWorker("c")).rejects.toThrow( new TypeError('"c" worker not found') ); // Check `getD1Database()` let binding: unknown = await mf.getD1Database("DB"); expect(binding).toBeDefined(); await expect(() => mf.getD1Database("DO")).rejects.toThrow( new TypeError(`No D1 database binding named "DO" found in "a" worker.`) ); await expect(() => mf.getD1Database("DB", "c")).rejects.toThrow( new TypeError(`No D1 database binding named "DB" found in "c" worker.`) ); // Check `getDurableObjectNamespace()` binding = await mf.getDurableObjectNamespace("DO"); expect(binding).toBeDefined(); await expect(() => mf.getDurableObjectNamespace("DO", "c")).rejects.toThrow( new TypeError( `No Durable Object namespace binding named "DO" found in "c" worker.` ) ); // Check `getKVNamespace()` binding = await mf.getKVNamespace("KV", ""); expect(binding).toBeDefined(); await expect(() => mf.getKVNamespace("KV", "c")).rejects.toThrow( new TypeError(`No KV namespace binding named "KV" found in "c" worker.`) ); // Check `getQueueProducer()` binding = await mf.getQueueProducer("QUEUE", ""); expect(binding).toBeDefined(); await expect(() => mf.getQueueProducer("QUEUE", "c")).rejects.toThrow( new TypeError( `No Queue producer binding named "QUEUE" found in "c" worker.` ) ); // Check `getR2Bucket()` binding = await mf.getR2Bucket("BUCKET", "b"); expect(binding).toBeDefined(); await expect(() => mf.getR2Bucket("BUCKET", "c")).rejects.toThrow( new TypeError(`No R2 bucket binding named "BUCKET" found in "c" worker.`) ); }); test("Miniflare: unsafeEvictDurableObject() resets in-memory state and preserves durable storage", async ({ expect, }) => { const mf = new Miniflare({ name: "do-worker", modules: true, script: ` export class Counter { constructor(state) { this.state = state; this.memoryCount = 0; } async fetch() { this.memoryCount += 1; const storageCount = ((await this.state.storage.get("count")) ?? 0) + 1; await this.state.storage.put("count", storageCount); return Response.json({ memoryCount: this.memoryCount, storageCount, }); } } export default { fetch(_request, env) { const id = env.COUNTER.idFromName("user-123"); return env.COUNTER.get(id).fetch("http://counter"); } }; `, durableObjects: { COUNTER: "Counter" }, }); useDispose(mf); let response = await mf.dispatchFetch("http://localhost"); expect(await response.json()).toEqual({ memoryCount: 1, storageCount: 1, }); await mf.unsafeEvictDurableObject("do-worker", "Counter", { name: "user-123", }); response = await mf.dispatchFetch("http://localhost"); expect(await response.json()).toEqual({ memoryCount: 1, storageCount: 2, }); }); test("Miniflare: allows direct access to workers", async ({ expect }) => { const mf = new Miniflare({ workers: [ { name: "a", script: `addEventListener("fetch", (e) => e.respondWith(new Response("a")))`, unsafeDirectSockets: [{ port: 0 }], }, { routes: ["*/*"], script: `addEventListener("fetch", (e) => e.respondWith(new Response("b")))`, }, { name: "c", script: `addEventListener("fetch", (e) => e.respondWith(new Response("c")))`, unsafeDirectSockets: [{ host: "127.0.0.1" }], }, { name: "d", compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class One extends WorkerEntrypoint { fetch() { return new Response("d:1"); } } export const two = { fetch() { return new Response("d:2"); } }; export const three = { fetch() { return new Response("d:2"); } }; `, unsafeDirectSockets: [{ entrypoint: "One" }, { entrypoint: "two" }], }, ], }); useDispose(mf); // Check can access workers as usual let res = await mf.dispatchFetch("http://localhost/"); expect(await res.text()).toBe("b"); // Check can access workers directly // (`undefined` worker name should default to entrypoint, not unnamed worker) const aURL = await mf.unsafeGetDirectURL(); const cURL = await mf.unsafeGetDirectURL("c"); res = await fetch(aURL); expect(await res.text()).toBe("a"); res = await fetch(cURL); expect(await res.text()).toBe("c"); // Check can access workers directly with different entrypoints const d1URL = await mf.unsafeGetDirectURL("d", "One"); const d2URL = await mf.unsafeGetDirectURL("d", "two"); res = await fetch(d1URL); expect(await res.text()).toBe("d:1"); res = await fetch(d2URL); expect(await res.text()).toBe("d:2"); // Can can only access configured for direct access await expect(mf.unsafeGetDirectURL("z")).rejects.toThrow( new TypeError('"z" worker not found') ); await expect(mf.unsafeGetDirectURL("")).rejects.toThrow( new TypeError('Direct access disabled in "" worker for default entrypoint') ); await expect(mf.unsafeGetDirectURL("d", "three")).rejects.toThrow( new TypeError('Direct access disabled in "d" worker for "three" entrypoint') ); }); test("Miniflare: allows RPC between multiple instances", async ({ expect }) => { const mf1 = new Miniflare({ unsafeDirectSockets: [{ entrypoint: "TestEntrypoint" }], compatibilityFlags: ["experimental"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class TestEntrypoint extends WorkerEntrypoint { ping() { return "pong"; } } `, }); useDispose(mf1); const testEntrypointUrl = await mf1.unsafeGetDirectURL("", "TestEntrypoint"); const mf2 = new Miniflare({ serviceBindings: { SERVICE: { external: { address: testEntrypointUrl.host, http: {} } }, }, compatibilityFlags: ["experimental"], modules: true, script: ` export default { async fetch(request, env, ctx) { const result = await env.SERVICE.ping(); return new Response(result); } } `, }); useDispose(mf2); const res = await mf2.dispatchFetch("http://placeholder"); expect(await res.text()).toBe("pong"); }); // Only test `MINIFLARE_WORKERD_PATH` on Unix. The test uses a Node.js script // with a shebang, directly as the replacement `workerd` binary, which won't // work on Windows. const isWindows = process.platform === "win32"; const unixSerialTest = isWindows ? test.skip : test.sequential; unixSerialTest( "Miniflare: MINIFLARE_WORKERD_PATH overrides workerd path", async ({ expect, onTestFinished }) => { const workerdPath = path.join(FIXTURES_PATH, "little-workerd.mjs"); const original = process.env.MINIFLARE_WORKERD_PATH; process.env.MINIFLARE_WORKERD_PATH = workerdPath; onTestFinished(() => { // Setting key/values pairs on `process.env` coerces values to strings if (original === undefined) delete process.env.MINIFLARE_WORKERD_PATH; else process.env.MINIFLARE_WORKERD_PATH = original; }); const mf = new Miniflare({ script: "" }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe( "When I grow up, I want to be a big workerd!" ); } ); // When workerd exits before sending all listen events on FD3, Miniflare should // detect this and throw ERR_RUNTIME_FAILURE instead of hanging indefinitely. // See https://github.com/cloudflare/workers-sdk/issues/14077 unixSerialTest( "Miniflare: throws ERR_RUNTIME_FAILURE when workerd exits before all sockets are ready", async ({ expect, onTestFinished }) => { const workerdPath = path.join(FIXTURES_PATH, "crashing-workerd.mjs"); const original = process.env.MINIFLARE_WORKERD_PATH; process.env.MINIFLARE_WORKERD_PATH = workerdPath; onTestFinished(() => { if (original === undefined) { delete process.env.MINIFLARE_WORKERD_PATH; } else { process.env.MINIFLARE_WORKERD_PATH = original; } }); const mf = new Miniflare({ script: "" }); onTestFinished(() => mf.dispose().catch(() => {})); await expect(mf.ready).rejects.toThrow(MiniflareCoreError); await expect(mf.ready).rejects.toThrow(/Workers runtime failed to start/); } ); const TIMEZONE_WORKER = ` export default { fetch() { return Response.json({ tz: Intl.DateTimeFormat().resolvedOptions().timeZone, }); } } `; test.sequential("Miniflare: workerd subprocess defaults to TZ=UTC to match production", async ({ expect, }) => { // Deliberately set the host TZ to a non-UTC value, otherwise the test // would falsely pass: the test-runner's vitest globalSetup already sets // `process.env.TZ = "UTC"`, which would propagate to workerd via the // inherited `process.env`. vi.stubEnv("TZ", "America/Chicago"); const mf = new Miniflare({ modules: true, script: TIMEZONE_WORKER }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.json()).toEqual({ tz: "UTC" }); }); // Skipped on Windows because workerd-on-Windows always reports `UTC` from // `Intl.DateTimeFormat().resolvedOptions().timeZone` regardless of the `TZ` // env var. A diagnostic probe on Windows CI showed that every value tried // (`America/Chicago`, `Asia/Tokyo`, `Europe/London`, `Etc/GMT+5`, `Etc/UTC`, // `UTC`, `PST8PDT`) all resolved to `UTC`. This is a workerd limitation on // Windows; the override mechanism itself (passing the env var through) is // platform-independent and exercised on macOS/Linux. unixSerialTest( "Miniflare: unsafeRuntimeEnv overrides the default workerd subprocess env", async ({ expect }) => { vi.stubEnv("TZ", "UTC"); const mf = new Miniflare({ modules: true, script: TIMEZONE_WORKER, unsafeRuntimeEnv: { TZ: "America/Chicago" }, }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost"); expect(await res.json()).toEqual({ tz: "America/Chicago" }); } ); test("Miniflare: exits cleanly", async ({ expect }) => { const miniflarePath = require.resolve("miniflare"); const result = childProcess.spawn( process.execPath, [ "--no-warnings", // Hide experimental warnings "-e", ` const { Miniflare, Log, LogLevel } = require(${JSON.stringify(miniflarePath)}); const mf = new Miniflare({ verbose: true, modules: true, script: \`export default { fetch() { return new Response("body"); } }\` }); (async () => { const res = await mf.dispatchFetch("http://placeholder/"); const text = await res.text(); process.send(text); process.disconnect(); })(); `, ], { stdio: [/* in */ "ignore", /* out */ "pipe", /* error */ "pipe", "ipc"], } ); // Make sure workerd started const [message] = await once(result, "message"); expect(message).toBe("body"); // Check exit doesn't output anything const closePromise = once(result, "close"); result.kill("SIGINT"); assert(result.stdout !== null && result.stderr !== null); const stdout = await text(result.stdout); const stderr = await text(result.stderr); await closePromise; expect(stdout).toBe(""); expect(stderr).toBe(""); }); test("Miniflare: supports unsafe eval bindings", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: `export default { fetch(req, env, ctx) { const three = env.UNSAFE_EVAL.eval("2 + 1"); const fn = env.UNSAFE_EVAL.newFunction( "return \`the computed value is \${n}\`", "", "n" ); return new Response(fn(three)); } }`, unsafeEvalBinding: "UNSAFE_EVAL", }); useDispose(mf); const response = await mf.dispatchFetch("http://localhost"); expect(response.ok).toBe(true); expect(await response.text()).toBe("the computed value is 3"); }); test("Miniflare: supports wrapped bindings", async ({ expect }) => { const store = new Map(); const mf = new Miniflare({ workers: [ { wrappedBindings: { MINI_KV: { scriptName: "mini-kv", bindings: { NAMESPACE: "ns" }, }, }, modules: true, script: `export default { async fetch(request, env, ctx) { await env.MINI_KV.set("key", "value"); const value = await env.MINI_KV.get("key"); await env.MINI_KV.delete("key"); const emptyValue = await env.MINI_KV.get("key"); await env.MINI_KV.set("key", "another value"); return Response.json({ value, emptyValue }); } }`, }, { name: "mini-kv", serviceBindings: { async STORE(request) { const { pathname } = new URL(request.url); const key = pathname.substring(1); if (request.method === "GET") { const value = store.get(key); const status = value === undefined ? 404 : 200; return new Response(value ?? null, { status }); } else if (request.method === "PUT") { const value = await request.text(); store.set(key, value); return new Response(null, { status: 204 }); } else if (request.method === "DELETE") { store.delete(key); return new Response(null, { status: 204 }); } else { return new Response(null, { status: 405 }); } }, }, modules: true, script: ` class MiniKV { constructor(env) { this.STORE = env.STORE; this.baseURL = "http://x/" + (env.NAMESPACE ?? "") + ":"; } async get(key) { const res = await this.STORE.fetch(this.baseURL + key); return res.status === 404 ? null : await res.text(); } async set(key, body) { await this.STORE.fetch(this.baseURL + key, { method: "PUT", body }); } async delete(key) { await this.STORE.fetch(this.baseURL + key, { method: "DELETE" }); } } export default function (env) { return new MiniKV(env); } `, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost/"); expect(await res.json()).toEqual({ value: "value", emptyValue: null }); expect(store).toEqual(new Map([["ns:key", "another value"]])); }); test("Miniflare: check overrides default bindings with bindings from wrapped binding designator", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { WRAPPED: { scriptName: "binding", entrypoint: "wrapped", bindings: { B: "overridden b" }, }, }, modules: true, script: `export default { fetch(request, env, ctx) { return env.WRAPPED(); } }`, }, { name: "binding", modules: true, bindings: { A: "default a", B: "default b" }, script: `export function wrapped(env) { return () => Response.json(env); }`, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost/"); expect(await res.json()).toEqual({ A: "default a", B: "overridden b" }); }); test("Miniflare: checks uses compatibility and outbound configuration of binder", async ({ expect, }) => { const workers: WorkerOptions[] = [ { compatibilityDate: "2022-03-21", // Default-on date for `global_navigator` compatibilityFlags: ["nodejs_compat"], wrappedBindings: { WRAPPED: "binding" }, modules: true, script: `export default { fetch(request, env, ctx) { return env.WRAPPED(); } }`, outboundService(request) { return new Response(`outbound:${request.url}`); }, }, { name: "binding", modules: [ { type: "ESModule", path: "index.mjs", contents: `export default function () { return async () => { const typeofNavigator = typeof navigator; let importedNode = false; try { await import("node:util"); importedNode = true; } catch {} const outboundRes = await fetch("http://placeholder/"); const outboundText = await outboundRes.text(); return Response.json({ typeofNavigator, importedNode, outboundText }); } }`, }, ], }, ]; const mf = new Miniflare({ workers }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost/"); expect(await res.json()).toEqual({ typeofNavigator: "object", importedNode: true, outboundText: "outbound:http://placeholder/", }); const fetchMock = createFetchMock(); fetchMock.disableNetConnect(); fetchMock .get("http://placeholder") .intercept({ path: "/" }) .reply(200, "mocked"); workers[0].compatibilityDate = "2022-03-20"; workers[0].compatibilityFlags = []; workers[0].outboundService = undefined; workers[0].fetchMock = fetchMock; await mf.setOptions({ workers }); res = await mf.dispatchFetch("http://localhost/"); expect(await res.json()).toEqual({ typeofNavigator: "undefined", importedNode: false, outboundText: "mocked", }); }); test("Miniflare: cannot call getWorker() on wrapped binding worker", async ({ expect, }) => { const mf = new Miniflare({ workers: [ { wrappedBindings: { WRAPPED: "binding" }, modules: true, script: `export default { fetch(request, env, ctx) { return env.WRAPPED; } }`, }, { name: "binding", modules: true, script: `export default function () { return "🎁"; }`, }, ], }); useDispose(mf); await expect(mf.getWorker("binding")).rejects.toThrow( new TypeError( '"binding" is being used as a wrapped binding, and cannot be accessed as a worker' ) ); }); test("Miniflare: prohibits invalid wrapped bindings", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: "" }); useDispose(mf); // Check prohibits using entrypoint worker await expect( mf.setOptions({ name: "a", modules: true, script: "", wrappedBindings: { WRAPPED: { scriptName: "a", entrypoint: "wrapped" }, }, }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "a" for wrapped binding because it\'s the entrypoint.\n' + 'Ensure "a" isn\'t the first entry in the `workers` array.' ) ); // Check prohibits using service worker await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", script: "" }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it\'s a service worker.\n' + 'Ensure "binding" sets `modules` to `true` or an array of modules' ) ); // Check prohibits multiple modules await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", modules: [ { type: "ESModule", path: "index.mjs", contents: "" }, { type: "ESModule", path: "dep.mjs", contents: "" }, ], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it isn\'t a single module.\n' + 'Ensure "binding" doesn\'t include unbundled `import`s.' ) ); // Check prohibits non-ES-modules await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", modules: [{ type: "CommonJS", path: "index.cjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it isn\'t a single ES module' ) ); // Check prohibits Durable Object bindings await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" }, durableObjects: { OBJECT: { scriptName: "binding", className: "TestObject" }, }, }, { name: "binding", modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it is bound to with Durable Object bindings.\n' + 'Ensure other workers don\'t define Durable Object bindings to "binding".' ) ); // Check prohibits service bindings await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: { scriptName: "binding", entrypoint: "wrapped" }, }, serviceBindings: { SERVICE: "binding" }, }, { name: "binding", modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it is bound to with service bindings.\n' + 'Ensure other workers don\'t define service bindings to "binding".' ) ); // Check prohibits compatibility date and flags await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", compatibilityDate: "2023-11-01", modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it defines a compatibility date.\n' + "Wrapped bindings use the compatibility date of the worker with the binding." ) ); await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", compatibilityFlags: ["nodejs_compat"], modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it defines compatibility flags.\n' + "Wrapped bindings use the compatibility flags of the worker with the binding." ) ); // Check prohibits outbound service await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", outboundService() { assert.fail(); }, modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_INVALID_WRAPPED", 'Cannot use "binding" for wrapped binding because it defines an outbound service.\n' + "Wrapped bindings use the outbound service of the worker with the binding." ) ); // Check prohibits cyclic wrapped bindings await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED: "binding" } }, { name: "binding", wrappedBindings: { WRAPPED: "binding" }, // Simple cycle modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_CYCLIC", "Generated workerd config contains cycles. Ensure wrapped bindings don't have bindings to themselves." ) ); await expect( mf.setOptions({ workers: [ { modules: true, script: "", wrappedBindings: { WRAPPED1: "binding-1" }, }, { name: "binding-1", wrappedBindings: { WRAPPED2: "binding-2" }, modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, { name: "binding-2", wrappedBindings: { WRAPPED3: "binding-3" }, modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, { name: "binding-3", wrappedBindings: { WRAPPED1: "binding-1" }, // Multi-step cycle modules: [{ type: "ESModule", path: "index.mjs", contents: "" }], }, ], }) ).rejects.toThrow( new MiniflareCoreError( "ERR_CYCLIC", "Generated workerd config contains cycles. Ensure wrapped bindings don't have bindings to themselves." ) ); }); test("Miniflare: getCf() returns a standard cf object", async ({ expect }) => { const mf = new Miniflare({ script: "", modules: true }); useDispose(mf); const cf = await mf.getCf(); expect(cf).toMatchObject({ colo: "DFW", city: "Austin", regionCode: "TX", }); }); test("Miniflare: getCf() returns a user provided cf object", async ({ expect, }) => { const mf = new Miniflare({ script: "", modules: true, cf: { myFakeField: "test", }, }); useDispose(mf); const cf = await mf.getCf(); expect(cf).toEqual({ myFakeField: "test" }); }); test("Miniflare: dispatchFetch() can override cf", async ({ expect }) => { const mf = new Miniflare({ script: "export default { fetch(request) { return Response.json(request.cf) } }", modules: true, cf: { myFakeField: "test", }, }); useDispose(mf); const cf = await mf.dispatchFetch("http://example.com/", { cf: { myFakeField: "test2" }, }); const cfJson = (await cf.json()) as { myFakeField: string }; expect(cfJson.myFakeField).toEqual("test2"); }); test("Miniflare: CF-Connecting-IP is injected", async ({ expect }) => { const mf = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }", modules: true, cf: { myFakeField: "test", }, }); useDispose(mf); const ip = await mf.dispatchFetch("http://example.com/"); // Tracked in https://github.com/cloudflare/workerd/issues/3310 if (!isWindows) { expect(await ip.text()).toEqual("127.0.0.1"); } else { expect(await ip.text()).toEqual(""); } }); test("Miniflare: CF-Connecting-IP is injected (ipv6)", async ({ expect }) => { const mf = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }", modules: true, cf: { myFakeField: "test", }, host: "::1", }); useDispose(mf); const ip = await mf.dispatchFetch("http://example.com/"); // Tracked in https://github.com/cloudflare/workerd/issues/3310 if (!isWindows) { expect(await ip.text()).toEqual("::1"); } else { expect(await ip.text()).toEqual(""); } }); test("Miniflare: CF-Connecting-IP is preserved when present", async ({ expect, }) => { const mf = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get('CF-Connecting-IP')) } }", modules: true, cf: { myFakeField: "test", }, }); useDispose(mf); const ip = await mf.dispatchFetch("http://example.com/", { headers: { "CF-Connecting-IP": "128.0.0.1", }, }); expect(await ip.text()).toEqual("128.0.0.1"); }); // regression test for https://github.com/cloudflare/workers-sdk/issues/7924 // The "server" service just returns the value of the CF-Connecting-IP header which would normally be added by Miniflare. If you send a request to with no such header, Miniflare will add one. // The "client" service makes an outbound request with a fake CF-Connecting-IP header to the "server" service. If the outbound stripping happens then this header will not make it to the "server" service // so its response will contain the header added by Miniflare. If the stripping is turned off then the response from the "server" service will contain the fake header. test("Miniflare: strips CF-Connecting-IP", async ({ expect }) => { const server = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }", modules: true, }); const serverUrl = await server.ready; const client = new Miniflare({ script: `export default { fetch(request) { return fetch('${serverUrl.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`, modules: true, }); useDispose(client); useDispose(server); const landingPage = await client.dispatchFetch("http://example.com/"); // The CF-Connecting-IP header value of "fake-value" should be stripped by Miniflare, and should be replaced with a generic 127.0.0.1 expect(await landingPage.text()).not.toEqual("fake-value"); }); test("Miniflare: does not strip CF-Connecting-IP when configured", async ({ expect, }) => { const server = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get(`CF-Connecting-IP`)) } }", modules: true, }); const serverUrl = await server.ready; const client = new Miniflare({ script: `export default { fetch(request) { return fetch('${serverUrl.href}', {headers: {"CF-Connecting-IP":"fake-value"}}) } }`, modules: true, stripCfConnectingIp: false, }); useDispose(client); useDispose(server); const landingPage = await client.dispatchFetch("http://example.com/"); expect(await landingPage.text()).toEqual("fake-value"); }); // Test for https://github.com/cloudflare/workers-sdk/issues/4367 // The CF-Worker header should be added to outbound fetch requests to match production behavior test("Miniflare: adds CF-Worker header to outbound requests with zone option", async ({ expect, }) => { const server = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get(`CF-Worker`)) } }", modules: true, }); const serverUrl = await server.ready; const client = new Miniflare({ name: "my-worker", zone: "my-zone.example.com", script: `export default { fetch(request) { return fetch('${serverUrl.href}') } }`, modules: true, }); useDispose(client); useDispose(server); const response = await client.dispatchFetch("http://example.com/"); // The CF-Worker header should be set to the zone value when provided expect(await response.text()).toEqual("my-zone.example.com"); }); test("Miniflare: CF-Worker header defaults to worker-name.example.com when zone not set", async ({ expect, }) => { const server = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get(`CF-Worker`)) } }", modules: true, }); const serverUrl = await server.ready; const client = new Miniflare({ name: "my-worker", // No zone set, should default to `${worker-name}.example.com` script: `export default { fetch(request) { return fetch('${serverUrl.href}') } }`, modules: true, }); useDispose(client); useDispose(server); const response = await client.dispatchFetch("http://example.com/"); // The CF-Worker header should default to `${worker-name}.example.com` when no zone is specified expect(await response.text()).toEqual("my-worker.example.com"); }); test("Miniflare: CF-Worker header defaults to worker.example.com when neither zone nor name set", async ({ expect, }) => { const server = new Miniflare({ script: "export default { fetch(request) { return new Response(request.headers.get(`CF-Worker`)) } }", modules: true, }); const serverUrl = await server.ready; const client = new Miniflare({ // No name or zone set, should default to "worker.example.com" script: `export default { fetch(request) { return fetch('${serverUrl.href}') } }`, modules: true, }); useDispose(client); useDispose(server); const response = await client.dispatchFetch("http://example.com/"); // The CF-Worker header should default to "worker.example.com" when neither zone nor name is specified expect(await response.text()).toEqual("worker.example.com"); }); test("Miniflare: can use module fallback service", async ({ expect }) => { const modulesRoot = "/"; const modules: Record> = { "/virtual/a.mjs": { esModule: ` import { b } from "./dir/b.mjs"; export default "a" + b; `, }, "/virtual/dir/b.mjs": { esModule: 'export { default as b } from "./c.cjs";', }, "/virtual/dir/c.cjs": { commonJsModule: 'module.exports = "c" + require("./sub/d.cjs");', }, "/virtual/dir/sub/d.cjs": { commonJsModule: 'module.exports = "d";', }, }; const mf = new Miniflare({ host: "localhost", unsafeModuleFallbackService(request) { const resolveMethod = request.headers.get("X-Resolve-Method"); assert(resolveMethod === "import" || resolveMethod === "require"); const url = new URL(request.url); const specifier = url.searchParams.get("specifier"); assert(specifier !== null); const maybeModule = modules[specifier]; if (maybeModule === undefined) return new Response(null, { status: 404 }); const name = path.posix.relative(modulesRoot, specifier); return new Response(JSON.stringify({ name, ...maybeModule })); }, workers: [ { name: "a", routes: ["*/a"], compatibilityFlags: ["export_commonjs_default"], modulesRoot, modules: [ { type: "ESModule", path: "/virtual/index.mjs", contents: ` import a from "./a.mjs"; export default { async fetch() { return new Response(a); } } `, }, ], unsafeUseModuleFallbackService: true, }, { name: "b", routes: ["*/b"], compatibilityFlags: ["export_commonjs_default"], modulesRoot, modules: [ { type: "ESModule", path: "/virtual/index.mjs", contents: ` export default { async fetch() { try { await import("./a.mjs"); return new Response(null, { status: 204 }); } catch (e) { return new Response(String(e), { status: 500 }); } } } `, }, ], }, ], }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost/a"); expect(await res.text()).toBe("acd"); // Check fallback service ignored if not explicitly enabled res = await mf.dispatchFetch("http://localhost/b"); expect(res.status).toBe(500); expect(await res.text()).toBe('Error: No such module "virtual/a.mjs".'); }); test("Miniflare: can use module fallback service with V2 protocol", async ({ expect, }) => { // V2 protocol uses file:// URLs instead of paths // Module keys are paths (without the /bundle prefix that workerd adds) const modules: Record> = { "/virtual/a.mjs": { esModule: ` import { b } from "./dir/b.mjs"; export default "a" + b; `, }, "/virtual/dir/b.mjs": { esModule: 'export { default as b } from "./c.cjs";', }, "/virtual/dir/c.cjs": { commonJsModule: 'module.exports = "c" + require("./sub/d.cjs");', }, "/virtual/dir/sub/d.cjs": { commonJsModule: 'module.exports = "d";', }, }; const mf = new Miniflare({ async unsafeModuleFallbackService(request) { // V2 protocol uses POST with JSON body instead of GET with query params assert(request.method === "POST", "V2 protocol should use POST method"); const body = await request.json(); assertIsV2ModuleFallbackProtocol(body); assert( body.type === "import" || body.type === "require", `Expected type to be "import" or "require", got "${body.type}"` ); assert( body.specifier.startsWith("file://"), `V2 specifier should be a file:// URL, got "${body.specifier}"` ); // V2 specifier is a file:// URL like "file:///bundle/virtual/a.mjs" // Extract the path and strip the "/bundle" prefix to match our modules map const specifierUrl = new URL(body.specifier); const modulePath = specifierUrl.pathname.replace(/^\/bundle/, ""); const maybeModule = modules[modulePath]; if (maybeModule === undefined) { return new Response(null, { status: 404 }); } // V2 protocol expects name to match the full URL specifier return Response.json({ name: body.specifier, ...maybeModule }); }, workers: [ { name: "a", routes: ["*/a"], compatibilityFlags: ["export_commonjs_default", "new_module_registry"], modulesRoot: "/", modules: [ { type: "ESModule", path: "/virtual/index.mjs", contents: ` import a from "./a.mjs"; export default { async fetch() { return new Response(a); } } `, }, ], unsafeUseModuleFallbackService: true, }, ], }); useDispose(mf); const res = await mf.dispatchFetch("http://localhost/a"); expect(await res.text()).toBe("acd"); }); test("Miniflare: respects rootPath for path-valued options", async ({ expect, }) => { const tmp = await useTmp(); const aPath = path.join(tmp, "a"); const bPath = path.join(tmp, "b"); await fs.mkdir(aPath); await fs.mkdir(bPath); await fs.writeFile(path.join(aPath, "1.txt"), "one text"); await fs.writeFile(path.join(aPath, "1.bin"), "one data"); await fs.writeFile(path.join(aPath, "add.wasm"), ADD_WASM_MODULE); await fs.writeFile(path.join(bPath, "2.txt"), "two text"); await fs.writeFile(path.join(tmp, "3.txt"), "three text"); const mf = new Miniflare({ rootPath: tmp, kvPersist: "kv", workers: [ { name: "a", rootPath: "a", routes: ["*/a"], textBlobBindings: { TEXT: "1.txt" }, dataBlobBindings: { DATA: "1.bin" }, wasmBindings: { ADD: "add.wasm" }, // WASM bindings aren't supported by modules workers script: `addEventListener("fetch", (event) => { event.respondWith(Response.json({ text: TEXT, data: new TextDecoder().decode(DATA), result: new WebAssembly.Instance(ADD).exports.add(1, 2) })); });`, }, { name: "b", rootPath: "b", routes: ["*/b"], textBlobBindings: { TEXT: "2.txt" }, sitePath: ".", script: `addEventListener("fetch", (event) => { event.respondWith(Response.json({ text: TEXT, manifest: Object.keys(__STATIC_CONTENT_MANIFEST) })); });`, }, { name: "c", routes: ["*/c"], textBlobBindings: { TEXT: "3.txt" }, kvNamespaces: { NAMESPACE: "namespace" }, modules: true, script: `export default { async fetch(request, env, ctx) { await env.NAMESPACE.put("key", "value"); return Response.json({ text: env.TEXT }); } }`, }, ], }); useDispose(mf); let res = await mf.dispatchFetch("http://localhost/a"); expect(await res.json()).toEqual({ text: "one text", data: "one data", result: 3, }); res = await mf.dispatchFetch("http://localhost/b"); expect(await res.json()).toEqual({ text: "two text", manifest: ["2.txt"], }); res = await mf.dispatchFetch("http://localhost/c"); expect(await res.json()).toEqual({ text: "three text", }); expect(existsSync(path.join(tmp, "kv", "namespace"))).toBe(true); // Check persistence URLs not resolved relative to root path await mf.setOptions({ rootPath: tmp, kvPersist: url.pathToFileURL(path.join(tmp, "kv")).href, kvNamespaces: { NAMESPACE: "namespace" }, modules: true, script: `export default { async fetch(request, env, ctx) { return new Response(await env.NAMESPACE.get("key")); } }`, }); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("value"); // Check only resolves root path once for single worker options (with relative // root path) useCwd(tmp); await mf.setOptions({ rootPath: "a", textBlobBindings: { TEXT: "1.txt" }, script: 'addEventListener("fetch", (event) => event.respondWith(new Response(TEXT)));', }); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("one text"); }); test("Miniflare: custom Node service binding", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: ` export default { fetch(request, env) { return env.CUSTOM.fetch(request, { headers: { "custom-header": "foo" } }); } }`, serviceBindings: { CUSTOM: { node: (req, res) => { res.end( `Response from custom Node service binding. The value of "custom-header" is "${req.headers["custom-header"]}".` ); }, }, }, }); useDispose(mf); const response = await mf.dispatchFetch("http://localhost"); const text = await response.text(); expect(text).toBe( `Response from custom Node service binding. The value of "custom-header" is "foo".` ); }); test("Miniflare: custom Node outbound service", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: ` export default { fetch(request, env) { return fetch(request, { headers: { "custom-header": "foo" } }); } }`, outboundService: { node: (req, res) => { res.end( `Response from custom Node outbound service. The value of "custom-header" is "foo".` ); }, }, }); useDispose(mf); const response = await mf.dispatchFetch("http://localhost"); const text = await response.text(); expect(text).toBe( `Response from custom Node outbound service. The value of "custom-header" is "foo".` ); }); test("Miniflare: setOptions: can restart workerd multiple times in succession", async ({ expect, }) => { // Regression test for https://github.com/cloudflare/workers-sdk/issues/11675 // EBADF errors can occur when spawning a new workerd process if the stdio // pipes from the previous process are not properly cleaned up. This is // especially relevant when other parts of the system (like worker_threads // used by Miniflare or other Vite plugins) are also managing file descriptors. // While this test doesn't reliably reproduce the race condition, it validates // that the restart mechanism works correctly after the fix. const mf = new Miniflare({ port: 0, modules: true, script: `export default { fetch() { return new Response("version 1"); } }`, }); useDispose(mf); // First request to ensure initial startup is complete let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("version 1"); // Perform multiple rapid setOptions calls to trigger workerd restarts. // This tests that the stdio pipe cleanup in dispose() works correctly. for (let i = 2; i <= 5; i++) { await mf.setOptions({ port: 0, modules: true, script: `export default { fetch() { return new Response("version ${i}"); } }`, }); res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe(`version ${i}`); } }); test("Miniflare: MINIFLARE_WORKERD_CONFIG_DEBUG controls workerd config file creation", async ({ expect, onTestFinished, }) => { const originalEnv = process.env.MINIFLARE_WORKERD_CONFIG_DEBUG; const configFilePath = "workerd-config.json"; // Clean up any existing config file if (existsSync(configFilePath)) { await fs.unlink(configFilePath); } onTestFinished(async () => { if (originalEnv === undefined) { delete process.env.MINIFLARE_WORKERD_CONFIG_DEBUG; } else { process.env.MINIFLARE_WORKERD_CONFIG_DEBUG = originalEnv; } if (existsSync(configFilePath)) { await fs.unlink(configFilePath); } }); // ensure the config file is not created without the flag delete process.env.MINIFLARE_WORKERD_CONFIG_DEBUG; let mf = new Miniflare({ modules: true, script: `export default { fetch() { return new Response("Hello World"); } }`, }); // Trigger workerd config serialization by dispatching a request let response = await mf.dispatchFetch("http://localhost"); // seems like miniflare doesn't like it if you don't read the response await response.text(); // config file should not be created when MINIFLARE_WORKERD_CONFIG_DEBUG is not set expect(existsSync(configFilePath)).toBe(false); await mf.dispose(); // ensure the config file is created with the flag process.env.MINIFLARE_WORKERD_CONFIG_DEBUG = configFilePath; mf = new Miniflare({ modules: true, script: `export default { fetch() { return new Response("Hello World"); } }`, }); response = await mf.dispatchFetch("http://localhost"); await response.text(); // workerd-config.json should be created when MINIFLARE_WORKERD_CONFIG_DEBUG=true expect(existsSync(configFilePath)).toBe(true); await mf.dispose(); }); // https://github.com/cloudflare/workers-sdk/issues/13013 test("Miniflare: dispatchFetch handles POST/PUT with non-2xx status", async ({ expect, }) => { const mf = new Miniflare({ modules: true, script: `export default { async fetch(request) { const url = new URL(request.url); const status = parseInt(url.searchParams.get("status") ?? "200"); return Response.json( { method: request.method, status }, { status } ); } }`, }); useDispose(mf); // GET + non-2xx (worked before the fix too) for (const status of [400, 401, 403, 404, 500]) { const res = await mf.dispatchFetch(`http://localhost/?status=${status}`); expect(res.status).toBe(status); expect(await res.json()).toEqual({ method: "GET", status }); } // POST + 2xx for (const status of [200, 201]) { const res = await mf.dispatchFetch(`http://localhost/?status=${status}`, { method: "POST", body: JSON.stringify({ data: "test" }), }); expect(res.status).toBe(status); expect(await res.json()).toEqual({ method: "POST", status }); } // POST + non-2xx (previously threw "fetch failed" due to undici bug) for (const status of [400, 401, 403, 404, 500]) { const res = await mf.dispatchFetch(`http://localhost/?status=${status}`, { method: "POST", body: JSON.stringify({ data: "test" }), }); expect(res.status).toBe(status); expect(await res.json()).toEqual({ method: "POST", status }); } // PUT + non-2xx (also affected by the same bug) for (const status of [400, 401, 403, 404, 500]) { const res = await mf.dispatchFetch(`http://localhost/?status=${status}`, { method: "PUT", body: JSON.stringify({ data: "test" }), }); expect(res.status).toBe(status); expect(await res.json()).toEqual({ method: "PUT", status }); } });