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 <lball@redhat.com>
This commit is contained in:
Lance Ball 2022-03-18 13:36:12 -04:00 committed by GitHub
parent b4d7aa9adb
commit 0362a4f11c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 50 deletions

30
package-lock.json generated
View File

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

View File

@ -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",

View File

@ -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

View File

@ -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<unknown> {
options = { ...options };
const headers = {
...message.headers,
...(options.headers as Record<string, string>),
};
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<unknown> {
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);
}
});
};
}

View File

@ -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<unknown> {
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<unknown> {
@ -83,7 +84,6 @@ describe("emitterFor() defaults", () => {
it("Supports HTTP binding, structured mode", () => {
function transport(message: Message): Promise<unknown> {
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<string, unknown>), ...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<string, unknown>), ...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<string, Record<string, string>>;
@ -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<string, Record<string, Record<string, string>>>;

View File

@ -6,7 +6,9 @@ module.exports = {
},
resolve: {
fallback: {
util: require.resolve("util/")
util: require.resolve("util/"),
http: false,
https: false
},
},
output: {