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:
parent
b4d7aa9adb
commit
0362a4f11c
|
@ -26,7 +26,7 @@
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"ajv-cli": "^5.0.0",
|
"ajv-cli": "^5.0.0",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"axios": "^0.21.3",
|
"axios": "^0.26.1",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
@ -1750,12 +1750,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.21.4",
|
"version": "0.26.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.14.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bail": {
|
"node_modules/bail": {
|
||||||
|
@ -3397,9 +3397,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.14.8",
|
"version": "1.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -10351,12 +10351,12 @@
|
||||||
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
"integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw=="
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.21.4",
|
"version": "0.26.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.14.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bail": {
|
"bail": {
|
||||||
|
@ -11639,9 +11639,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.8",
|
"version": "1.14.9",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
|
||||||
"integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==",
|
"integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"foreach": {
|
"foreach": {
|
||||||
|
|
|
@ -127,7 +127,7 @@
|
||||||
"@typescript-eslint/parser": "^4.29.0",
|
"@typescript-eslint/parser": "^4.29.0",
|
||||||
"ajv-cli": "^5.0.0",
|
"ajv-cli": "^5.0.0",
|
||||||
"ajv-formats": "^2.1.1",
|
"ajv-formats": "^2.1.1",
|
||||||
"axios": "^0.21.3",
|
"axios": "^0.26.1",
|
||||||
"chai": "~4.2.0",
|
"chai": "~4.2.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { ValidationError } from "./event/validation";
|
||||||
import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
|
import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
|
||||||
|
|
||||||
import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter";
|
import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter";
|
||||||
|
import { httpTransport } from "./transport/http";
|
||||||
import {
|
import {
|
||||||
Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory,
|
Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory,
|
||||||
Serializer, Deserializer } from "./message";
|
Serializer, Deserializer } from "./message";
|
||||||
|
@ -25,6 +26,7 @@ export {
|
||||||
MQTT,
|
MQTT,
|
||||||
MQTTMessageFactory,
|
MQTTMessageFactory,
|
||||||
emitterFor,
|
emitterFor,
|
||||||
|
httpTransport,
|
||||||
Emitter,
|
Emitter,
|
||||||
// From Constants
|
// From Constants
|
||||||
CONSTANTS
|
CONSTANTS
|
||||||
|
|
|
@ -3,20 +3,61 @@
|
||||||
SPDX-License-Identifier: Apache-2.0
|
SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Message, Options } from "../..";
|
import { Socket } from "net";
|
||||||
import axios from "axios";
|
import http, { OutgoingHttpHeaders } from "http";
|
||||||
|
import https, { RequestOptions } from "https";
|
||||||
|
|
||||||
export function axiosEmitter(sink: string) {
|
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 function(message: Message, options?: Options): Promise<unknown> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
options = { ...options };
|
options = { ...options };
|
||||||
const headers = {
|
|
||||||
...message.headers,
|
// TODO: Callers should be able to set any Node.js RequestOptions
|
||||||
...(options.headers as Record<string, string>),
|
const opts: RequestOptions = {
|
||||||
|
method: "POST",
|
||||||
|
headers: {...message.headers, ...options.headers as OutgoingHttpHeaders},
|
||||||
};
|
};
|
||||||
delete options.headers;
|
try {
|
||||||
return axios.post(sink, message.body, {
|
const response = {
|
||||||
headers: headers,
|
body: "",
|
||||||
...options,
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
import "mocha";
|
import "mocha";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import nock from "nock";
|
import nock from "nock";
|
||||||
import axios from "axios";
|
import axios, { AxiosRequestHeaders } from "axios";
|
||||||
import request from "superagent";
|
import request from "superagent";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
|
||||||
import CONSTANTS from "../../src/constants";
|
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 DEFAULT_CE_CONTENT_TYPE = CONSTANTS.DEFAULT_CE_CONTENT_TYPE;
|
||||||
const sink = "https://cloudevents.io/";
|
const sink = "https://cloudevents.io/";
|
||||||
|
@ -38,7 +39,7 @@ export const fixture = new CloudEvent({
|
||||||
});
|
});
|
||||||
|
|
||||||
function axiosEmitter(message: Message, options?: Options): Promise<unknown> {
|
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> {
|
function superagentEmitter(message: Message, options?: Options): Promise<unknown> {
|
||||||
|
@ -83,7 +84,6 @@ describe("emitterFor() defaults", () => {
|
||||||
|
|
||||||
it("Supports HTTP binding, structured mode", () => {
|
it("Supports HTTP binding, structured mode", () => {
|
||||||
function transport(message: Message): Promise<unknown> {
|
function transport(message: Message): Promise<unknown> {
|
||||||
console.error(message);
|
|
||||||
// A structured message will have the application/cloudevents+json header
|
// A structured message will have the application/cloudevents+json header
|
||||||
expect(message.headers["content-type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
expect(message.headers["content-type"]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE);
|
||||||
const body = JSON.parse(message.body as string);
|
const body = JSON.parse(message.body as string);
|
||||||
|
@ -101,9 +101,8 @@ describe("emitterFor() defaults", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("HTTP Transport Binding for emitterFactory", () => {
|
function setupMock(uri: string) {
|
||||||
beforeEach(() => {
|
nock(uri)
|
||||||
nock(sink)
|
|
||||||
.post("/")
|
.post("/")
|
||||||
.reply(function (uri: string, body: nock.Body) {
|
.reply(function (uri: string, body: nock.Body) {
|
||||||
// return the request body and the headers so they can be
|
// return the request body and the headers so they can be
|
||||||
|
@ -114,20 +113,38 @@ describe("HTTP Transport Binding for emitterFactory", () => {
|
||||||
const returnBody = { ...(body as Record<string, unknown>), ...this.req.headers };
|
const returnBody = { ...(body as Record<string, unknown>), ...this.req.headers };
|
||||||
return [201, returnBody];
|
return [201, returnBody];
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("HTTP Transport Binding for emitterFactory", () => {
|
||||||
|
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", () => {
|
describe("Axios", () => {
|
||||||
testEmitter(axiosEmitter, "data");
|
testEmitterBinary(axiosEmitter, "data");
|
||||||
|
testEmitterStructured(axiosEmitter, "data");
|
||||||
});
|
});
|
||||||
describe("SuperAgent", () => {
|
describe("SuperAgent", () => {
|
||||||
testEmitter(superagentEmitter, "body");
|
testEmitterBinary(superagentEmitter, "body");
|
||||||
|
testEmitterStructured(superagentEmitter, "body");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Got", () => {
|
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 () => {
|
it("Works as a binary event emitter", async () => {
|
||||||
const emitter = emitterFor(fn);
|
const emitter = emitterFor(fn);
|
||||||
const response = (await emitter(fixture)) as Record<string, Record<string, string>>;
|
const response = (await emitter(fixture)) as Record<string, Record<string, string>>;
|
||||||
|
@ -137,7 +154,9 @@ function testEmitter(fn: TransportFunction, bodyAttr: string) {
|
||||||
}
|
}
|
||||||
assertBinary(body);
|
assertBinary(body);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testEmitterStructured(fn: TransportFunction, bodyAttr: string) {
|
||||||
it("Works as a structured event emitter", async () => {
|
it("Works as a structured event emitter", async () => {
|
||||||
const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED });
|
const emitter = emitterFor(fn, { binding: HTTP, mode: Mode.STRUCTURED });
|
||||||
const response = (await emitter(fixture)) as Record<string, Record<string, Record<string, string>>>;
|
const response = (await emitter(fixture)) as Record<string, Record<string, Record<string, string>>>;
|
||||||
|
|
|
@ -6,7 +6,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
fallback: {
|
fallback: {
|
||||||
util: require.resolve("util/")
|
util: require.resolve("util/"),
|
||||||
|
http: false,
|
||||||
|
https: false
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|
Loading…
Reference in New Issue