Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/non-waking-container-forward.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/containers": patch
---

Add a non-starting `fetchIfRunning()` helper that only proxies requests to already-running, healthy containers without waking stopped containers.
22 changes: 22 additions & 0 deletions src/lib/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,24 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
// HTTP
// ============

/**
* Forward a request to the container only if it is already running and healthy.
*
* This method never starts the container or waits for ports to become ready.
*/
public async fetchIfRunning(request: Request, port = this.defaultPort): Promise<Response> {
if (port === undefined) {
throw new Error('No port specified for container fetch');
}

const state = await this.state.getState();
if (!this.container.running || state.status !== 'healthy') {
return new Response('Container is not running', { status: 503 });
}

return await this.forwardToTcpPort(request, port);
}

/**
* Send a request to the container (HTTP or WebSocket) using standard fetch API signature
*
Expand Down Expand Up @@ -1193,6 +1211,10 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
}
}

return await this.forwardToTcpPort(request, port);
}

private async forwardToTcpPort(request: Request, port: number): Promise<Response> {
const tcpPort = this.container.getTcpPort(port);

// Create URL for the container request
Expand Down
65 changes: 65 additions & 0 deletions src/tests/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,71 @@ describe('Container', () => {
expect(tcpPort.fetch).toHaveBeenCalledWith(expect.any(String), expect.any(Object));
});

for (const { name, running, status } of [
{ name: 'stopped container', running: false, status: 'stopped' },
{ name: 'running but non-healthy container', running: true, status: 'running' },
] as const) {
test(`fetchIfRunning should not start or forward ${name}`, async ({ mockCtx, container }) => {
mockCtx.container.running = running;
mockCtx.storage.get.mockResolvedValue({ status, lastChange: Date.now() });

const response = await container.fetchIfRunning(
new Request('https://example.com/test'),
8080
);

expect(response.status).toBe(503);
expect(mockCtx.container.start).not.toHaveBeenCalled();
expect(mockCtx.container.getTcpPort).not.toHaveBeenCalled();
});
}

test('fetchIfRunning should use default port when port is omitted', async ({
mockCtx,
container,
}) => {
mockCtx.container.running = true;
mockCtx.storage.get.mockResolvedValue({ status: 'healthy', lastChange: Date.now() });

const response = await container.fetchIfRunning(new Request('https://example.com/test'));

expect(response.status).toBe(200);
expect(mockCtx.container.getTcpPort).toHaveBeenCalledWith(8080);
});

test('fetchIfRunning should bypass the startup path', async ({ mockCtx, container }) => {
using containerFetchSpy = vi.spyOn(container, 'containerFetch');
mockCtx.container.running = true;
mockCtx.storage.get.mockResolvedValue({ status: 'healthy', lastChange: Date.now() });

const response = await container.fetchIfRunning(new Request('https://example.com/test'), 8080);

expect(response.status).toBe(200);
expect(containerFetchSpy).not.toHaveBeenCalled();
expect(mockCtx.container.start).not.toHaveBeenCalled();
expect(mockCtx.container.getTcpPort).toHaveBeenCalledWith(8080);
});

test('fetchIfRunning should forward WebSocket requests', async ({ mockCtx, container }) => {
mockCtx.container.running = true;
mockCtx.storage.get.mockResolvedValue({ status: 'healthy', lastChange: Date.now() });

const response = await container.fetchIfRunning(
new Request('https://example.com/ws', {
headers: new Headers({
Upgrade: 'websocket',
Connection: 'Upgrade',
}),
}),
8080
);

const tcpPort = mockCtx.container.getTcpPort.mock.results[0].value;
expect(tcpPort.fetch).toHaveBeenCalled();
expect(webSocketPairSpy).toHaveBeenCalledOnce();
expect(response.status).toBe(200);
});

test('containerFetch should return 429 when startup is rate limited', async ({ container }) => {
const mockRequest = new Request('https://example.com/test', { method: 'GET' });
using startSpy = vi
Expand Down
Loading