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: {