Mocking Server Side HTTP in Playwright with mockttp —…
    Neura MarketNeura Market/Stable Diffusion
    ChatGPTChatGPTClaudeClaudeGeminiGeminiCursorCursorGrokGrokPerplexityPerplexityStable DiffusionStable Diffusion
    DeepSeekDeepSeekCoPilotCoPilotMidjourneyMidjourney
    View All Directories
    OverviewPromptsBlogVideosGuidesCoursesCommunityModelsLoRAsComfyUI WorkflowsTrending
    Stable DiffusionBlogMocking Server Side HTTP in Playwright with mockttp
    Back to Blog
    Mocking Server Side HTTP in Playwright with mockttp
    playwright

    Mocking Server Side HTTP in Playwright with mockttp

    Simon Knott May 22, 2026
    0 views

    Mock outgoing HTTP from your server in Playwright tests by running a per-worker mockttp forward proxy.


    title: Mocking Server Side HTTP in Playwright with mockttp published: true description: Mock outgoing HTTP from your server in Playwright tests by running a per-worker mockttp forward proxy. tags: playwright, testing, typescript, mockttp

    cover_image: https://direct_url_to_image.jpg

    Use a ratio of 100:42 for best results.

    published_at: 2026-05-22 13:09 +0000


    I've been building a little recipe app for my household. Along the way I needed to mock out third-party APIs in my Playwright tests. For browser requests that's page.route(), and it works wonderfully. For server-side HTTP calls, it doesn't.

    I tried mock-only branches inside the server code. That got messy fast, especially when the same API had to behave differently in different tests. So I went looking for something cleaner.

    I landed on a forward proxy that intercepts outgoing traffic from the server, configured from the test code. Here's how it works.

    The Idea

    Instead of patching modules or adding if (isUnderTest) branches, we mock at the network boundary. The app makes the same HTTPS calls it always makes. A forward proxy sits in front and decides whether to answer with a canned response or pass the request through.

    What makes this work is the HTTP_PROXY / HTTPS_PROXY env var convention. Almost every HTTP client honours it.

    I use mockttp as the proxy. It generates a TLS certificate on the fly, runs an HTTP/HTTPS forward proxy in-process, and exposes a fluent rule builder for mocks and interception. We start one per Playwright worker, and every test configures it as it likes.

    The snippets below show a Node dev server, but none of this is Node-specific. Any language whose HTTP client respects HTTP_PROXY / HTTPS_PROXY works the same way: Python, Go, Ruby, Rust, .NET. Java needs -Dhttps.proxyHost / -Dhttps.proxyPort instead of env vars. You'll also need to teach each runtime to trust mockttp's certificate: SSL_CERT_FILE for Python and Ruby, SSL_CERT_DIR for Go, and so on.

    Setting Up the Fixture

    We set up mockttp as a Playwright custom fixture, so every test gets a clean, isolated set of mocks. Here's the whole integration in one file:

    // tests/fixtures.ts
    import * as fs from "node:fs/promises";
    import { tmpdir } from "node:os";
    import { join } from "node:path";
    import { test as base } from "@playwright/test";
    import * as mockttp from "mockttp";
    
    export const test = base.extend<
      { mocks: mockttp.Mockttp },
      { mockttp: { server: mockttp.Mockttp; caCertPath: string } }
    >({
      mockttp: [async ({}, use) => {
        const ca = await mockttp.generateCACertificate();
        const dir = await fs.mkdtemp(join(tmpdir(), "mockttp-ca-"));
        const caCertPath = join(dir, "ca.pem");
        await fs.writeFile(caCertPath, ca.cert);
    
        const server = mockttp.getLocal({ https: { cert: ca.cert, key: ca.key } });
        await server.start();
        await server.forUnmatchedRequest().thenPassThrough();
        await use({ server, caCertPath });
        await server.stop();
      }, { scope: "worker" }],
    
      mocks: async ({ mockttp }, use) => {
        await use(mockttp.server);
        await mockttp.server.reset();
        await mockttp.server.forUnmatchedRequest().thenPassThrough();
      },
    });
    
    export { expect } from "@playwright/test";
    

    A quick tour of the moving parts:

    1. The mockttp fixture is worker-scoped and returns both the server and the certificate path. One mockttp server and one TLS certificate per Playwright worker.
    2. The mocks fixture is test-scoped and exposes the server directly, so tests can write mocks.forPost(...). Each test gets a clean rule set thanks to mockttp.server.reset() on teardown.
    3. forUnmatchedRequest().thenPassThrough() is our default, so requests we didn't mock still go through. reset() clears it too, so we re-add it after.

    Spinning Up the Dev Server

    For mocking to work, the dev server has to know about the mockttp instance. It needs HTTPS_PROXY pointing at our proxy and NODE_EXTRA_CA_CERTS pointing at our certificate, both of which are per-worker. So we spawn it ourselves from a worker fixture, pass in the right env, and override baseURL so page.goto("/") still works.

    // tests/fixtures.ts
    import * as childProcess from "node:child_process";
    // ...other imports as above
    
    export const test = base.extend<
      { mocks: mockttp.Mockttp },
      {
        mockttp: { server: mockttp.Mockttp; caCertPath: string };
        devServer: { baseURL: string };
      }
    >({
      // ...mockttp and mocks fixtures from above...
    
      devServer: [async ({ mockttp }, use) => {
        const child = childProcess.spawn(
          "npm", ["start"],
          {
            stdio: ["ignore", "pipe", "pipe"],
            env: {
              ...process.env,
              HTTP_PROXY: mockttp.server.url,
              HTTPS_PROXY: mockttp.server.url,
              NODE_USE_ENV_PROXY: "1",
              NODE_EXTRA_CA_CERTS: mockttp.caCertPath,
              PORT: "0",
            },
          },
        );
        const baseURL = await new Promise<string>((resolve, reject) => {
          let buf = "";
          child.stdout!.on("data", (chunk) => {
            buf += chunk.toString();
            const m = buf.match(/Listening on (https?:\S+)/);
            if (m) resolve(m[1]);
          });
          child.once("exit", (code) =>
            reject(new Error(`dev server exited with code ${code}`)),
          );
        });
        await use({ baseURL });
        child.kill("SIGTERM");
      }, { scope: "worker" }],
    
      baseURL: async ({ devServer }, use) => {
        await use(devServer.baseURL);
      },
    });
    

    A tour of the env vars we pass in:

    1. HTTP_PROXY and HTTPS_PROXY point at the worker's mockttp via server.url.
    2. NODE_USE_ENV_PROXY=1 makes Node's built-in fetch honor HTTPS_PROXY (Node 20+).
    3. NODE_EXTRA_CA_CERTS only accepts a file path, which is why the worker fixture writes the certificate to one.
    4. PORT: "0" lets the OS hand the dev server a free port, so workers don't collide. We parse the actual port out of stdout once the server is up.

    Why Not Playwright's webServer?

    Playwright ships a webServer config option that boots your app before tests run:

    // playwright.config.ts
    export default defineConfig({
      webServer: {
        command: "npm start",
        port: 5173,
      },
    });
    

    It's convenient, but it starts one process for the whole test run, before any worker fixture has a chance to set up. The test worker that starts the proxy doesn't exist yet at that point, so webServer can't pick up HTTPS_PROXY or NODE_EXTRA_CA_CERTS. That's why we spawn the server from a fixture instead.

    Writing a Test

    Now the fun part. Mocks live right next to the assertions that depend on them:

    // tests/recipes.spec.ts
    import { test, expect } from "./fixtures";
    
    test("summarises a recipe", async ({ page, mocks }) => {
      const endpoint = await mocks
        .forPost("https://api.openai.com/v1/chat/completions")
        .thenJson(200, { choices: [{ message: { content: "Pasta." } }] });
    
      await page.goto("/recipes/123");
      await page.getByRole("button", { name: "Summarise" }).click();
      await expect(page.getByText("Pasta.")).toBeVisible();
    
      expect(await endpoint.getSeenRequests()).toHaveLength(1);
    });
    

    No shared mocks/ directory, no "which canned response is this test using?" question. The test owns its mocks.

    Beyond HTTP: A Test Channel for Anything

    Here's a neat trick once you have this set up. The proxy isn't just for real third-party APIs. It can act as a general-purpose RPC channel between your server code and the test runner. Anything that's awkward to mock (clocks, random IDs, feature flags, filesystem state) can be replaced with a small HTTP call to a fictitious http://playwright/... URL when the app is under test:

    // app/server/clock.ts
    export async function now(): Promise<Date> {
      // 👉 use whatever env var your project sets in test mode
      if (process.env.NODE_ENV === "test") {
        const res = await fetch("http://playwright/clock");
        return new Date(await res.text());
      }
      return new Date();
    }
    

    Then in the test, you respond to it:

    // tests/billing.spec.ts
    import { test } from "./fixtures";
    
    test("renews subscription on the day it expires", async ({ page, mocks }) => {
      await mocks
        .forGet("http://playwright/clock")
        .thenReply(200, "2030-01-15T00:00:00Z");
    
      // …drive the UI; the server now thinks it's January 15th.
    });
    

    Gotchas

    A few more notes:

    1. Forgotten mocks hit the real network. Consider swapping the passthrough default for a loud thenReply(599, …).
    2. In-process apps need undici's ProxyAgent instead of the env vars. The env-variable trick only works for child processes.
    3. mockttp has an admin-server / remote control mode. If you can't spawn the app from inside your worker process, you can connect to a mockttp instance running in a separate process.
    4. mockttp.reset() clears everything, including the passthrough. Remember to re-add it on teardown.

    That's it. Happy mocking!

    Tags

    playwrighttestingtypescriptmockttp

    Comments

    More Blog

    View all
    Context bankruptcy: The case for strategic forgetting for AI Agentsai

    Context bankruptcy: The case for strategic forgetting for AI Agents

    Most of us have seen a coding agent fail to complete a task we know it can do. We just don't...

    J
    James O'Reilly
    Parallel Compliance Engine: Drive-to-Sheets Multi-Agent Orchestrationgooglecloud

    Parallel Compliance Engine: Drive-to-Sheets Multi-Agent Orchestration

    When building Generative AI applications, developers often encounter a massive bottleneck: sequential...

    A
    Aryan Irani
    Is It Ethical to Post and Ask About Circuits on Dev.to?discuss

    Is It Ethical to Post and Ask About Circuits on Dev.to?

    I’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...

    C
    codebunny20
    The One-Click Exporter: AI Studio Antigravity, Probed to Its Limitsagents

    The One-Click Exporter: AI Studio Antigravity, Probed to Its Limits

    What nobody tells you about exporting your multi-agent prototype to a local workspace. Every...

    L
    leslysandra
    Guarding the till while autonomous data agents do the diggingagenticarchitect

    Guarding the till while autonomous data agents do the digging

    Autonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...

    S
    Sireesha Pulipati
    Return on Attention: Why AI Code Reviews Are Wearing Us Outai

    Return on Attention: Why AI Code Reviews Are Wearing Us Out

    PR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.

    C
    christine

    Stay up to date

    Get the latest Stable Diffusion prompts, rules, and resources delivered to your inbox weekly.

    Neura Market LogoNeura Market

    Discover the best AI prompts, plugins, and resources for Stable Diffusion and more.

    Content Types

    • Rules
    • Prompts
    • MCPs
    • Agents
    • Guides

    Platforms

    • ChatGPT Directory
    • Claude Directory
    • Gemini Directory
    • Cursor Directory
    • Grok Directory
    • Perplexity Directory
    • DeepSeek Directory
    • CoPilot Directory
    • Stable Diffusion Directory
    • Midjourney Directory
    • All Directories

    Resources

    • Blog
    • Documentation
    • Help Center
    • Marketplace

    Legal

    • Privacy Policy
    • Terms of Service

    © 2026 Neura Market. All rights reserved.

    |

    Not affiliated with any AI platform vendors.

    Ready-made automations for this

    Workflows from the Neura Market marketplace related to this Stable Diffusion resource

    • Extract Business Leads from Google Maps with Dumpling AI to Google Sheetsn8n · Free · Related topic
    • Extract Text from Images & PDFs via Telegram with Mistral OCR to Markdownn8n · Free · Related topic
    • Cold Email Icebreakers from Local Business Search with GP-4 and Dumpling AIn8n · Free · Related topic
    • eBay Enhances Data Access for AI Agents with MCP Server Integrationn8n · Free · Related topic
    Browse all workflows