Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: CI

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
# branches: [ "master" ]
# pull_request:
# branches: [ "master" ]

jobs:
build:
Expand Down
132 changes: 123 additions & 9 deletions lib/wait-on.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,10 @@ const Joi = require('joi');
const https = require('https');
const net = require('net');
const util = require('util');
const axiosPkg = require('axios').default;
const { isBoolean, isEmpty, negate, noop, once, partial, pick, zip } = require('lodash/fp');
const { NEVER, combineLatest, from, merge, throwError, timer } = require('rxjs');
const { distinctUntilChanged, map, mergeMap, scan, startWith, take, takeWhile } = require('rxjs/operators');

// force http adapter for axios, otherwise if using jest/jsdom xhr might
// be used and it logs all errors polluting the logs
const axios = axiosPkg.create({ adapter: 'http' });
const isNotABoolean = negate(isBoolean);
const isNotEmpty = negate(isEmpty);
const fstat = promisify(fs.stat);
Expand Down Expand Up @@ -310,19 +306,137 @@ function createHTTP$({ validatedOpts, output }, resource) {

async function httpCallSucceeds(output, httpOptions) {
try {
const result = await axios(httpOptions);
const { url, method, headers, auth, timeout, httpsAgent, validateStatus, followRedirect, socketPath } = httpOptions;

// Handle Unix socket connections - fetch doesn't support this, so use Node.js http modules
if (socketPath) {
return await httpCallSucceedsWithSocket(output, httpOptions);
}

// Build fetch options
const fetchOptions = {
method: method.toUpperCase(),
headers: { ...headers },
agent: httpsAgent,
redirect: followRedirect ? 'follow' : 'manual'
};

// Handle authentication
if (auth && auth.username && auth.password) {
const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
fetchOptions.headers.Authorization = `Basic ${credentials}`;
}

// Handle timeout
const controller = new AbortController();
let timeoutId;
if (timeout) {
timeoutId = setTimeout(() => controller.abort(), timeout);
fetchOptions.signal = controller.signal;
}

const response = await fetch(url, fetchOptions);

if (timeoutId) {
clearTimeout(timeoutId);
}

// Handle custom status validation or default validation
let isValid;
if (validateStatus) {
isValid = validateStatus(response.status);
} else {
if (followRedirect) {
// When following redirects, we expect the final response to be successful (2xx)
// If we get a redirect status here, it means fetch didn't follow it properly
isValid = response.status >= 200 && response.status < 300;
} else {
// When not following redirects, 3xx should be treated as invalid
if (response.status >= 300 && response.status < 400) {
output(` HTTP(S) result for ${url}: redirect not followed (status: ${response.status})`);
return false;
}
isValid = response.status >= 200 && response.status < 300;
}
}

let responseData;
try {
responseData = method.toLowerCase() === 'get' ? await response.text() : undefined;
} catch (_e) { // eslint-disable-line no-unused-vars
responseData = undefined; // Don't fail if we can't read response body
}

output(
` HTTP(S) result for ${httpOptions.url}: ${util.inspect(
pick(['status', 'statusText', 'headers', 'data'], result)
)}`
` HTTP(S) result for ${url}: ${util.inspect({
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
data: responseData
})}`
);
return true;

return isValid;
} catch (err) {
output(` HTTP(S) error for ${httpOptions.url} ${err.toString()}`);
return false;
}
}

// Handle Unix socket HTTP requests using Node.js http/https modules
async function httpCallSucceedsWithSocket(output, httpOptions) {
const { url, method, headers, auth, timeout, socketPath, validateStatus } = httpOptions;
const http = require('http');

return new Promise((resolve) => {
const options = {
socketPath,
path: url,
method: method.toUpperCase(),
headers: { ...headers },
timeout
};

// Handle authentication
if (auth && auth.username && auth.password) {
const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
options.headers.Authorization = `Basic ${credentials}`;
}

const req = http.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const isValid = validateStatus ? validateStatus(res.statusCode) : (res.statusCode >= 200 && res.statusCode < 300);

output(
` HTTP(S) result for ${socketPath}${url}: ${util.inspect({
status: res.statusCode,
statusText: res.statusMessage,
headers: res.headers,
data: method.toLowerCase() === 'get' ? data : undefined
})}`
);

resolve(isValid);
});
});

req.on('error', (err) => {
output(` HTTP(S) error for ${socketPath}${url} ${err.toString()}`);
resolve(false);
});

req.on('timeout', () => {
output(` HTTP(S) timeout for ${socketPath}${url}`);
req.destroy();
resolve(false);
});

req.end();
});
}

function createTCP$({ validatedOpts: { delay, interval, tcpTimeout, reverse, simultaneous }, output }, resource) {
const tcpPath = extractPath(resource);
const checkFn = reverse ? negateAsync(tcpExists) : tcpExists;
Expand Down
Loading