From 0362a4f11c7bdc74a3a9a05b5bb4a94516b15a44 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Fri, 18 Mar 2022 13:36:12 -0400 Subject: [PATCH] feat!: add http transport and remove axios (#481) * feat: add builtin HTTP emitter Adds a builtin HTTP event emitter that can be used with `emitterFor()` to send events over HTTP without pulling in any additional dependencies. In the past we chose to keep this in our code base by considering axios a peer dependency - users were required to include it in their projects explicitly. In working on the HTTP emitter, it became more and more apparent that the axios emitter was probably no longer needed, and in fact I doubt it was really used at all. To use it, users would have been required to do this, since it isn't exported at the top level. const { axiosEmitter } = require("cloudevents/transport/http"); Based on this, I think the usage in the wild is probably very minimal, and I like the idea of eliminating this dependency. Signed-off-by: Lance Ball --- package-lock.json | 30 +++++------ package.json | 2 +- src/index.ts | 2 + src/transport/http/index.ts | 67 +++++++++++++++++++----- test/integration/emitter_factory_test.ts | 59 ++++++++++++++------- webpack.config.js | 4 +- 6 files changed, 114 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index cda2b99..bc21dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@typescript-eslint/parser": "^4.29.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", - "axios": "^0.21.3", + "axios": "^0.26.1", "chai": "~4.2.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", @@ -1750,12 +1750,12 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.8" } }, "node_modules/bail": { @@ -3397,9 +3397,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "dev": true, "funding": [ { @@ -10351,12 +10351,12 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", "dev": true, "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.14.8" } }, "bail": { @@ -11639,9 +11639,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "dev": true }, "foreach": { diff --git a/package.json b/package.json index 458d071..8ff4741 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "@typescript-eslint/parser": "^4.29.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", - "axios": "^0.21.3", + "axios": "^0.26.1", "chai": "~4.2.0", "eslint": "^7.32.0", "eslint-config-standard": "^16.0.3", diff --git a/src/index.ts b/src/index.ts index a8fc944..e09e9d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { ValidationError } from "./event/validation"; import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter"; +import { httpTransport } from "./transport/http"; import { Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory, Serializer, Deserializer } from "./message"; @@ -25,6 +26,7 @@ export { MQTT, MQTTMessageFactory, emitterFor, + httpTransport, Emitter, // From Constants CONSTANTS diff --git a/src/transport/http/index.ts b/src/transport/http/index.ts index 60eaad2..2ac2062 100644 --- a/src/transport/http/index.ts +++ b/src/transport/http/index.ts @@ -3,20 +3,61 @@ SPDX-License-Identifier: Apache-2.0 */ -import { Message, Options } from "../.."; -import axios from "axios"; +import { Socket } from "net"; +import http, { OutgoingHttpHeaders } from "http"; +import https, { RequestOptions } from "https"; -export function axiosEmitter(sink: string) { - return function (message: Message, options?: Options): Promise { - options = { ...options }; - const headers = { - ...message.headers, - ...(options.headers as Record), - }; - delete options.headers; - return axios.post(sink, message.body, { - headers: headers, - ...options, +import { Message, Options } from "../.."; +import { TransportFunction } from "../emitter"; + +/** + * httpTransport provides a simple HTTP Transport function, which can send a CloudEvent, + * encoded as a Message to the endpoint. The returned function can be used with emitterFor() + * to provide an event emitter, for example: + * + * const emitter = emitterFor(httpTransport("http://example.com")); + * emitter.emit(myCloudEvent) + * .then(resp => console.log(resp)); + * + * @param {string|URL} sink the destination endpoint for the event + * @returns {TransportFunction} a function which can be used to send CloudEvents to _sink_ + */ +export function httpTransport(sink: string | URL): TransportFunction { + const url = new URL(sink); + let base: any; + if (url.protocol === "https:") { + base = https; + } else if (url.protocol === "http:") { + base = http; + } else { + throw new TypeError(`unsupported protocol ${url.protocol}`); + } + return function(message: Message, options?: Options): Promise { + return new Promise((resolve, reject) => { + options = { ...options }; + + // TODO: Callers should be able to set any Node.js RequestOptions + const opts: RequestOptions = { + method: "POST", + headers: {...message.headers, ...options.headers as OutgoingHttpHeaders}, + }; + try { + const response = { + body: "", + headers: {}, + }; + const req = base.request(url, opts, (res: Socket) => { + res.setEncoding("utf-8"); + response.headers = (res as any).headers; + res.on("data", (chunk) => response.body += chunk); + res.on("end", () => { resolve(response); }); + }); + req.on("error", reject); + req.write(message.body); + req.end(); + } catch (err) { + reject(err); + } }); }; } diff --git a/test/integration/emitter_factory_test.ts b/test/integration/emitter_factory_test.ts index 49cf031..568f6cc 100644 --- a/test/integration/emitter_factory_test.ts +++ b/test/integration/emitter_factory_test.ts @@ -6,12 +6,13 @@ import "mocha"; import { expect } from "chai"; import nock from "nock"; -import axios from "axios"; +import axios, { AxiosRequestHeaders } from "axios"; import request from "superagent"; import got from "got"; import CONSTANTS from "../../src/constants"; -import { CloudEvent, emitterFor, HTTP, Mode, Message, Options, TransportFunction } from "../../src"; +import { CloudEvent, HTTP, Message, Mode, Options, TransportFunction, emitterFor, httpTransport } + from "../../src"; const DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE; const sink = "https://cloudevents.io/"; @@ -38,7 +39,7 @@ export const fixture = new CloudEvent({ }); function axiosEmitter(message: Message, options?: Options): Promise { - return axios.post(sink, message.body, { headers: message.headers, ...options }); + return axios.post(sink, message.body, { headers: message.headers as AxiosRequestHeaders, ...options }); } function superagentEmitter(message: Message, options?: Options): Promise { @@ -83,7 +84,6 @@ describe("emitterFor() defaults", () => { it("Supports HTTP binding, structured mode", () => { function transport(message: Message): Promise { - console.error(message); // A structured message will have the application/cloudevents+json header expect(message.headers["content-type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); const body = JSON.parse(message.body as string); @@ -101,33 +101,50 @@ describe("emitterFor() defaults", () => { }); }); +function setupMock(uri: string) { + nock(uri) + .post("/") + .reply(function (uri: string, body: nock.Body) { + // return the request body and the headers so they can be + // examined in the test + if (typeof body === "string") { + body = JSON.parse(body); + } + const returnBody = { ...(body as Record), ...this.req.headers }; + return [201, returnBody]; + }); +} + describe("HTTP Transport Binding for emitterFactory", () => { - beforeEach(() => { - nock(sink) - .post("/") - .reply(function (uri: string, body: nock.Body) { - // return the request body and the headers so they can be - // examined in the test - if (typeof body === "string") { - body = JSON.parse(body); - } - const returnBody = { ...(body as Record), ...this.req.headers }; - return [201, returnBody]; - }); + beforeEach(() => { setupMock(sink); }); + + describe("HTTPS builtin", () => { + testEmitterBinary(httpTransport(sink), "body"); + }); + + describe("HTTP builtin", () => { + setupMock("http://cloudevents.io"); + testEmitterBinary(httpTransport("http://cloudevents.io"), "body"); + setupMock("http://cloudevents.io"); + testEmitterStructured(httpTransport("http://cloudevents.io"), "body"); }); describe("Axios", () => { - testEmitter(axiosEmitter, "data"); + testEmitterBinary(axiosEmitter, "data"); + testEmitterStructured(axiosEmitter, "data"); }); describe("SuperAgent", () => { - testEmitter(superagentEmitter, "body"); + testEmitterBinary(superagentEmitter, "body"); + testEmitterStructured(superagentEmitter, "body"); }); + describe("Got", () => { - testEmitter(gotEmitter, "body"); + testEmitterBinary(gotEmitter, "body"); + testEmitterStructured(gotEmitter, "body"); }); }); -function testEmitter(fn: TransportFunction, bodyAttr: string) { +function testEmitterBinary(fn: TransportFunction, bodyAttr: string) { it("Works as a binary event emitter", async () => { const emitter = emitterFor(fn); const response = (await emitter(fixture)) as Record>; @@ -137,7 +154,9 @@ function testEmitter(fn: TransportFunction, bodyAttr: string) { } assertBinary(body); }); +} +function testEmitterStructured(fn: TransportFunction, bodyAttr: string) { it("Works as a structured event emitter", async () => { const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED }); const response = (await emitter(fixture)) as Record>>; diff --git a/webpack.config.js b/webpack.config.js index 2523e14..6b91cc5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,7 +6,9 @@ module.exports = { }, resolve: { fallback: { - util: require.resolve("util/") + util: require.resolve("util/"), + http: false, + https: false }, }, output: {