Skip to content

Cookies are deleted when a response is sent from the outboundByHost handler to the container. #199

@RahiReja

Description

@RahiReja

Summary

Using outboundByHost with interceptHttps = true appears to delete cookies that has been set using Set-Cookie, from the response that has been sent from the outboundBYHost handler. As a result when a response is send back from the container it is not including the cookies. If multiple Set-Cookie header present then also it is not sending the cookie .

Github Repo for reproducible code - https://github.com/RahiReja/egress-tests

Environment

  • Package: @cloudflare/containers@0.3.3
  • Wrangler: 4.81.0
  • Worker compatibility date: 2026-02-06
  • enableInternet = true
  • interceptHttps = true

Minimal shape of the setup

export class EgressTestContainer extends Container {
  defaultPort = 8080;
  sleepAfter = '3m';
  enableInternet = false;
  interceptHttps = true;

  // allowedHosts gates everything: only these hosts can reach outbound/internet.
  // 'by-host.com' is included so the outboundByHost handler can run.
  allowedHosts = ['allowed.com', 'by-host.com', '*.globtest.com'];
  deniedHosts = ['denied.com'];

  constructor(ctx: any, env: any) {
    super(ctx, env);
    this.entrypoint = ['node', 'server.js'];
    this.envVars = {
      NODE_EXTRA_CA_CERTS: '/etc/cloudflare/certs/cloudflare-containers-ca.crt',
    };
  }
}

EgressTestContainer.outboundByHost = {
  'by-host.com': (_req: Request) => {
    const res = new Response('outboundByHost: by-host.com');
    res.headers.append('Set-Cookie', 'test1=by-host1; Path=/');
    res.headers.append('Set-Cookie', 'test2=by-host2; Path=/');
    console.log(res.headers.getAll('Set-Cookie')); // Logs both cookies
    return res;
  },
  '*.globtest.com': (req: Request) => {
    return new Response('outboundByHost glob: ' + new URL(req.url).hostname);
  },
};

EgressTestContainer.outbound = (req: Request) => {
  return new Response('catch-all: ' + new URL(req.url).hostname);
};

export default {
  async fetch(
    request: Request,
    env: { CONTAINER: DurableObjectNamespace<EgressTestContainer> }
  ): Promise<Response> {
    try {
      const url = new URL(request.url);
      const id = url.searchParams.get('id') || 'singleton';
      const container = getContainer(env.CONTAINER, id);

      if (url.pathname === '/proxy') {
        return await container.containerFetch(request);
      }
      if (url.pathname === '/proxy_https') {
        const res = await container.containerFetch(request);
        // return await container.containerFetch(request);
        console.log('Handler result:', res);
        return res;
      }
      if (url.pathname === '/config/deny-host') {
        const hostname = url.searchParams.get('hostname');
        if (!hostname) {
          return new Response('hostname is required', { status: 400 });
        }

        await container.denyHost(hostname);
        return new Response('OK');
      }
      if (url.pathname === '/destroy') {
        await container.destroy();
        return new Response('Container killed');
      }
      if (url.pathname === '/status') {
        const state = await container.getState();
        return new Response(JSON.stringify(state, null, 2));
      }

      return new Response('Not Found');
    } catch (e) {
      console.error('Worker fetch error:', e);
      return new Response(`Worker error: ${e instanceof Error ? e.message : String(e)}`, {
        status: 500,
      });
    }
  },
};

Container Code

import { createServer, get } from 'http';
import { get as httpsGet } from 'https';

const server = createServer(function (req, res) {
  const url = new URL(req.url, 'http://localhost:8080');

  // Outbound HTTP proxy: the container makes an HTTP request to the given host.
  const proxyTarget = url.searchParams.get('proxy');
  if (proxyTarget) {
    get('http://' + proxyTarget, proxyRes => {
      let body = '';
      proxyRes.on('data', chunk => {
        body += chunk;
      });
      proxyRes.on('end', () => {
        res.writeHead(proxyRes.statusCode, { 'Content-Type': 'text/plain' });
        res.end(body);
      });
    }).on('error', err => {
      res.writeHead(520, { 'Content-Type': 'text/plain' });
      res.end('proxy error: ' + err.message);
    });
    return;
  }

  // Outbound HTTPS proxy: uses NODE_EXTRA_CA_CERTS (set via env) to trust
  // the Cloudflare containers CA for intercepted HTTPS.
  const proxyHttpsTarget = url.searchParams.get('proxy_https');
  if (proxyHttpsTarget) {
    httpsGet('https://' + proxyHttpsTarget, proxyRes => {
      let body = '';
      proxyRes.on('data', chunk => {
        body += chunk;
      });
      proxyRes.on('end', () => {
        res.writeHead(proxyRes.statusCode, { 'Content-Type': 'text/plain' });
        res.end(body);
      });
    }).on('error', err => {
      res.writeHead(520, { 'Content-Type': 'text/plain' });
      res.end('proxy_https error: ' + err.message);
    });
    return;
  }

  res.writeHead(200, {
    'Content-Type': 'text/plain',
    'Set-Cookie': ['cookie1=value1; Path=/', 'cookie2=value2; Path=/'],
  });

  res.end('Hello from egress test container');
});

server.listen(8080, function () {
  console.log('Egress test server listening on port 8080');
});

process.on('SIGTERM', () => {
  server.close(() => {
    process.exit(0);
  });
});

This code is identical to the GitHub Container Package example in the egress-tests directory with some modifications.

In the server.js file the proxyHttpsTarget is set to include a Set-Cookie header and the outboundByHost handler has been modified to add two cookie headers. In the index.ts file the /proxy_https route checks the response to verify the integrity of the cookies.

Expected behavior
The cookies added in the outbound handler must be present in the /proxyhttps response. If a fetch request is made in the outbound handler and the response contains multiple set-cookie values, these must be passed to the /proxyhttps response.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions