diff --git a/src/command/http-command.ts b/src/command/http-command.ts index d3a4c5c..525279a 100644 --- a/src/command/http-command.ts +++ b/src/command/http-command.ts @@ -107,7 +107,7 @@ const getInitialResult = () => ({ }); const allowedHttpProtocols = [ 'HTTP', 'HTTPS', 'HTTP2' ]; -const allowedHttpMethods = [ 'GET', 'HEAD' ]; +const allowedHttpMethods = [ 'GET', 'HEAD', 'OPTIONS' ]; const allowedIpVersions = [ 4, 6 ]; export const httpOptionsSchema = Joi.object({ @@ -145,7 +145,7 @@ export const urlBuilder = (options: HttpOptions): string => { return url; }; -export const httpCmd = (options: HttpOptions, resolverFn?: ResolverType): Request => { +export const httpCmd = (options: HttpOptions, resolverFn?: ResolverType): Request => { const url = urlBuilder(options); const dnsResolver = callbackify(dnsLookup(options.resolver, resolverFn), true); @@ -166,6 +166,7 @@ export const httpCmd = (options: HttpOptions, resolverFn?: ResolverType): Reques 'User-Agent': 'globalping probe (https://github.com/jsdelivr/globalping)', 'host': options.request.host ?? options.target, }, + ...(options.request.method === 'OPTIONS' && { body: '' }), // https://github.com/sindresorhus/got/issues/2394 setHost: false, throwHttpErrors: false, context: { @@ -215,7 +216,7 @@ export class HttpCommand implements CommandInterface { rawOutput = result.error; } else if (result.error) { rawOutput = `HTTP/${result.httpVersion} ${result.statusCode}\n${result.rawHeaders}\n\n${result.error}`; - } else if (cmdOptions.request.method === 'HEAD') { + } else if (cmdOptions.request.method === 'HEAD' || !result.rawBody) { rawOutput = `HTTP/${result.httpVersion} ${result.statusCode}\n${result.rawHeaders}`; } else { rawOutput = `HTTP/${result.httpVersion} ${result.statusCode}\n${result.rawHeaders}\n\n${result.rawBody}`; diff --git a/test/unit/command/http-command.test.ts b/test/unit/command/http-command.test.ts index b9f6027..5188833 100644 --- a/test/unit/command/http-command.test.ts +++ b/test/unit/command/http-command.test.ts @@ -450,6 +450,115 @@ describe('http command', () => { ]); }); + it('should respond with 200 on OPTIONS request and response with body', async () => { + nock('http://google.com').options('/').reply(200, function () { + const request = this.req as typeof this.req & {response: Response & {socket: { getPeerCertificate }}}; + request.response.httpVersion = '1.1'; + request.response.socket.getPeerCertificate = false; + return 'response body'; + }); + + const options = { + type: 'http' as const, + target: 'google.com', + inProgressUpdates: false, + protocol: 'HTTP', + request: { + method: 'OPTIONS', + path: '/', + query: '', + }, + ipVersion: 4, + }; + + const http = new HttpCommand(httpCmd); + await http.run(mockedSocket as any, 'measurement', 'test', options); + + expect(mockedSocket.emit.callCount).to.equal(1); + + expect(mockedSocket.emit.firstCall.args).to.deep.equal([ + 'probe:measurement:result', + { + testId: 'test', + measurementId: 'measurement', + result: { + status: 'finished', + resolvedAddress: '127.0.0.1', + headers: {}, + rawHeaders: null, + rawBody: 'response body', + rawOutput: 'HTTP/1.1 200\n\n\nresponse body', + truncated: false, + statusCode: 200, + statusCodeName: 'OK', + timings: { + total: 0, + download: 0, + firstByte: 0, + dns: 0, + tls: null, + tcp: 0, + }, + tls: null, + }, + }, + ]); + }); + + it('should respond with 200 on OPTIONS request and response without body', async () => { + nock('http://google.com').options('/').reply(200, function () { + const request = this.req as typeof this.req & {response: Response & {socket: { getPeerCertificate }}}; + request.response.httpVersion = '1.1'; + request.response.socket.getPeerCertificate = false; + }); + + const options = { + type: 'http' as const, + target: 'google.com', + inProgressUpdates: false, + protocol: 'HTTP', + request: { + method: 'OPTIONS', + path: '/', + query: '', + }, + ipVersion: 4, + }; + + const http = new HttpCommand(httpCmd); + await http.run(mockedSocket as any, 'measurement', 'test', options); + + expect(mockedSocket.emit.callCount).to.equal(1); + + expect(mockedSocket.emit.firstCall.args).to.deep.equal([ + 'probe:measurement:result', + { + testId: 'test', + measurementId: 'measurement', + result: { + status: 'finished', + resolvedAddress: '127.0.0.1', + headers: {}, + rawHeaders: null, + rawBody: null, + rawOutput: 'HTTP/1.1 200\n', + truncated: false, + statusCode: 200, + statusCodeName: 'OK', + timings: { + total: 0, + download: 0, + firstByte: 0, + dns: 0, + tls: null, + tcp: 0, + }, + tls: null, + }, + }, + ]); + }); + it('should respond with 400 with progress messages', async () => { nock('http://google.com').get('/400').times(3).reply(400, function () { const request = this.req as typeof this.req & {response: Response & {socket: { getPeerCertificate }}}; diff --git a/wallaby.js b/wallaby.js index 2a712a0..2db4d48 100644 --- a/wallaby.js +++ b/wallaby.js @@ -3,6 +3,7 @@ import * as url from 'node:url'; export default function wallaby () { const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + return { testFramework: 'mocha', files: [ @@ -12,7 +13,6 @@ export default function wallaby () { 'test/plugins/**/*', 'test/utils.ts', 'test/hooks.ts', - 'test/setup.ts', 'test/snapshots/**/*.json', 'package.json', ], @@ -22,7 +22,6 @@ export default function wallaby () { setup (w) { const path = require('path'); w.testFramework.files.unshift(path.resolve(process.cwd(), 'test/hooks.js')); - w.testFramework.files.unshift(path.resolve(process.cwd(), 'test/setup.js')); const mocha = w.testFramework; mocha.timeout(5000); },