Skip to content

Commit c6b2187

Browse files
committed
Fix Docker tunnel pull race condition
Previously we assumed that when pull() resolved it was complete. This is false. In reality we have to wait for the resulting stream to explicitly finish, so we had a subtle race condition here where the tunnel would not be available if it was required immediately after startup (e.g. in the tests) on the first run of the app, on a weak connection or whilst Docker hub was running a little slow.
1 parent 40c7c57 commit c6b2187

File tree

3 files changed

+29
-15
lines changed

3 files changed

+29
-15
lines changed

src/interceptors/docker/docker-tunnel-proxy.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import { getDockerHostAddress, isImageAvailable } from './docker-commands';
77
import { isDockerAvailable } from './docker-interception-services';
88
import { delay } from '../../util/promise';
99
import { reportError } from '../../error-tracking';
10+
import { waitForDockerStream } from './docker-utils';
1011

1112
const DOCKER_TUNNEL_IMAGE = "httptoolkit/docker-socks-tunnel:v1.1.0";
1213
const DOCKER_TUNNEL_LABEL = "tech.httptoolkit.docker.tunnel";
1314

1415
const getDockerTunnelContainerName = (proxyPort: number) =>
1516
`httptoolkit-docker-tunnel-${proxyPort}`;
1617

18+
const pullTunnelImage = (docker: Docker) =>
19+
docker.pull(DOCKER_TUNNEL_IMAGE)
20+
.then(stream => waitForDockerStream(docker, stream));
21+
1722
// Parallel mutation of a single Docker container's state is asking for trouble, so we use
1823
// a simple lock over all operations (across all proxes, not per-proxy, just for simplicity/safety).
1924
const containerMutex = new Mutex();
@@ -25,7 +30,7 @@ export async function prepareDockerTunnel() {
2530
await containerMutex.runExclusive(async () => {
2631
const docker = new Docker();
2732
if (await isImageAvailable(docker, DOCKER_TUNNEL_IMAGE)) return;
28-
else await docker.pull(DOCKER_TUNNEL_IMAGE).catch(console.warn);
33+
else await pullTunnelImage(docker).catch(console.warn);
2934
});
3035
}
3136

@@ -45,7 +50,7 @@ export function ensureDockerTunnelRunning(proxyPort: number) {
4550

4651
// Make sure we have the image available (should've been pre-pulled, but just in case)
4752
if (!await docker.getImage(DOCKER_TUNNEL_IMAGE).inspect().catch(() => false)) {
48-
await docker.pull(DOCKER_TUNNEL_IMAGE);
53+
await pullTunnelImage(docker);
4954
}
5055

5156
// Ensure we have a ready-to-use container here:
@@ -195,7 +200,7 @@ export async function getDockerTunnelPort(proxyPort: number): Promise<number> {
195200
return portCache[proxyPort]!;
196201
}
197202

198-
export async function refreshDockerTunnelPortCache(proxyPort: number, { force } = { force: false }): Promise<number> {
203+
async function refreshDockerTunnelPortCache(proxyPort: number, { force } = { force: false }): Promise<number> {
199204
console.log("Querying Docker tunnel port...");
200205
try {
201206
if (!force && _.isObject(portCache[proxyPort])) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Docker from 'dockerode';
2+
3+
export const waitForDockerStream = (
4+
docker: Docker,
5+
stream: NodeJS.ReadableStream
6+
) => new Promise<void>((resolve, reject) => {
7+
docker.modem.followProgress(stream, (
8+
err: Error | null,
9+
stream: Array<{ error?: string }>
10+
) => {
11+
if (err) reject(err);
12+
13+
const firstError = stream.find((msg) => !!msg.error);
14+
if (firstError) reject(new Error(firstError.error));
15+
16+
resolve();
17+
});
18+
});

test/interceptors/docker-attachment.spec.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { expect } from 'chai';
77
import * as Docker from 'dockerode';
88
import fetch from 'node-fetch';
99

10-
import { setupInterceptor, itIsAvailable } from './interceptor-test-utils';
1110
import { delay } from '../../src/util/promise';
11+
import { setupInterceptor, itIsAvailable } from './interceptor-test-utils';
12+
import { waitForDockerStream } from '../../src/interceptors/docker/docker-utils';
1213

1314
const docker = new Docker();
1415
const DOCKER_FIXTURES = path.join(__dirname, '..', 'fixtures', 'docker');
@@ -39,17 +40,7 @@ async function buildAndRun(dockerFolder: string, options: {
3940
if (data.stream) console.log(data.stream.replace(/\n$/, ''));
4041
});
4142

42-
// Wait for the build to complete without errors:
43-
await new Promise<void>((resolve, reject) => {
44-
docker.modem.followProgress(buildStream, (err: Error | null, stream: Array<{ error?: string }>) => {
45-
if (err) reject(err);
46-
47-
const firstError = stream.find((msg) => !!msg.error);
48-
if (firstError) reject(new Error(firstError.error));
49-
50-
resolve();
51-
});
52-
});
43+
await waitForDockerStream(docker, buildStream);
5344
console.log(`${dockerFolder} image built`);
5445

5546
// Run the container, using its default entrypoint

0 commit comments

Comments
 (0)