diff --git a/.changeset/twenty-dogs-carry.md b/.changeset/twenty-dogs-carry.md new file mode 100644 index 000000000..fc1e4f921 --- /dev/null +++ b/.changeset/twenty-dogs-carry.md @@ -0,0 +1,5 @@ +--- +"@onflow/transport-http": minor +--- + +Add http headers as option to in httpRequest diff --git a/packages/transport-http/src/http-request.js b/packages/transport-http/src/http-request.js index 624b11137..117aa7268 100644 --- a/packages/transport-http/src/http-request.js +++ b/packages/transport-http/src/http-request.js @@ -31,15 +31,14 @@ class HTTPRequestError extends Error { } /** - * Creates an HTTP Request to be sent to a REST Access API. + * Creates an HTTP Request to be sent to a REST Access API via Fetch API. * - * Supports the Fetch API on Web Browsers and Deno. - * Uses the Node HTTP(S) standard libraries for Node. - * - * @param {String} hostname - Access API Hostname - * @param {String} path - Path to the resource on the Access API - * @param {String} method - HTTP Method - * @param {Object} body - HTTP Request Body + * @param {Object} options - Options for the HTTP Request + * @param {String} options.hostname - Access API Hostname + * @param {String} options.path - Path to the resource on the Access API + * @param {String} options.method - HTTP Method + * @param {any} options.body - HTTP Request Body + * @param {Object | Headers} [options.headers] - HTTP Request Headers * * @returns JSON object response from Access API. */ @@ -48,6 +47,7 @@ export async function httpRequest({ path, method, body, + headers, retryLimit = 5, retryIntervalMs = 1000, }) { @@ -81,6 +81,7 @@ export async function httpRequest({ return fetchTransport(`${hostname}${path}`, { method: method, body: body ? JSON.stringify(body) : undefined, + headers, }) .then(async res => { if (res.ok) { @@ -100,13 +101,13 @@ export async function httpRequest({ statusCode: res.status, }) }) - .catch(e => { + .catch(async e => { if (e instanceof HTTPRequestError) { throw e } // Show AN error for all network errors - logger.log({ + await logger.log({ title: "Access Node Error", message: `The provided access node ${hostname} does not appear to be a valid REST/HTTP access node. Please verify that you are not unintentionally using a GRPC access node. diff --git a/packages/transport-http/src/http-request.test.js b/packages/transport-http/src/http-request.test.js new file mode 100644 index 000000000..e92d00e65 --- /dev/null +++ b/packages/transport-http/src/http-request.test.js @@ -0,0 +1,102 @@ +import * as fetchTransport from "node-fetch" +import * as logger from "@onflow/util-logger" +import {httpRequest} from "./http-request" + +describe("httpRequest", () => { + test("makes valid fetch request", async () => { + const spy = jest.spyOn(fetchTransport, "default") + spy.mockImplementation(async () => ({ + ok: true, + status: 200, + body: JSON.stringify({ + foo: "bar", + }), + async json() { + return JSON.parse(this.body) + }, + })) + + const opts = { + hostname: "https://example.com", + path: "/foo/bar", + body: "abc123", + method: "POST", + headers: { + Authorization: "Bearer 1RyXjsFJfU", + }, + } + + await httpRequest(opts) + + await expect(spy).toHaveBeenCalledWith(`${opts.hostname}${opts.path}`, { + method: opts.method, + body: JSON.stringify(opts.body), + headers: opts.headers, + }) + }) + + test("handles http error properly, throws HTTP error", async () => { + const spy = jest.spyOn(fetchTransport, "default") + spy.mockImplementation(async () => ({ + ok: false, + body: JSON.stringify({ + foo: "bar", + }), + async json() { + return JSON.parse(this.body) + }, + status: 400, + statusText: "foo bar", + })) + + const opts = { + hostname: "https://example.com", + path: "/foo/bar", + body: "abc123", + method: "POST", + headers: { + Authorization: "Bearer 1RyXjsFJfU", + }, + } + + await expect(httpRequest(opts)).rejects.toThrow("HTTP Request Error:") + }) + + test("handles fetch error properly, displays AN error", async () => { + const fetchSpy = jest.spyOn(fetchTransport, "default") + + fetchSpy.mockImplementation(() => + Promise.reject({ + ok: false, + body: JSON.stringify({ + foo: "bar", + }), + async json() { + return JSON.parse(this.body) + }, + status: 400, + statusText: "foo bar", + }) + ) + + const loggerSpy = jest.spyOn(logger, "log") + loggerSpy.mockImplementation(() => {}) + + const opts = { + hostname: "https://example.com", + path: "/foo/bar", + body: "abc123", + method: "POST", + headers: { + Authorization: "Bearer 1RyXjsFJfU", + }, + } + + await expect(httpRequest(opts)).rejects.toThrow("HTTP Request Error:") + expect(loggerSpy.mock.calls[0][0].title).toBe("Access Node Error") + }) + + afterEach(() => { + jest.restoreAllMocks() + }) +})