From e60563fb540c04d9ba751fea69c1fc0f1de598b5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 22 Jul 2020 14:22:03 +0530 Subject: consistent error handling for HTTP request (and some other things) --- src/util/amounts-test.ts | 4 +- src/util/amounts.ts | 2 +- src/util/http.ts | 267 ++++++++++++++++++++++++++++------------------- 3 files changed, 161 insertions(+), 112 deletions(-) (limited to 'src/util') diff --git a/src/util/amounts-test.ts b/src/util/amounts-test.ts index e10ee5962..afd8caa51 100644 --- a/src/util/amounts-test.ts +++ b/src/util/amounts-test.ts @@ -24,9 +24,7 @@ const jAmt = ( currency: string, ): AmountJson => ({ value, fraction, currency }); -const sAmt = ( - s: string -): AmountJson => Amounts.parseOrThrow(s); +const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s); test("amount addition (simple)", (t) => { const a1 = jAmt(1, 0, "EUR"); diff --git a/src/util/amounts.ts b/src/util/amounts.ts index 94aefb3cd..1e7f192f4 100644 --- a/src/util/amounts.ts +++ b/src/util/amounts.ts @@ -349,7 +349,7 @@ function mult(a: AmountJson, n: number): Result { n = n / 2; } else { n = (n - 1) / 2; - const r2 = add(acc, x) + const r2 = add(acc, x); if (r2.saturated) { return r2; } diff --git a/src/util/http.ts b/src/util/http.ts index bc054096a..abbc8df03 100644 --- a/src/util/http.ts +++ b/src/util/http.ts @@ -14,18 +14,26 @@ TALER; see the file COPYING. If not, see */ -import { Codec } from "./codec"; -import { OperationFailedError } from "../operations/errors"; - /** * Helpers for doing XMLHttpRequest-s that are based on ES6 promises. * Allows for easy mocking for test cases. */ +/** + * Imports + */ +import { Codec } from "./codec"; +import { OperationFailedError, makeErrorDetails } from "../operations/errors"; +import { TalerErrorCode } from "../TalerErrorCode"; +import { Logger } from "./logging"; + +const logger = new Logger("http.ts"); + /** * An HTTP response that is returned by all request methods of this library. */ export interface HttpResponse { + requestUrl: string; status: number; headers: Headers; json(): Promise; @@ -67,10 +75,20 @@ export class Headers { } /** - * The request library is bundled into an interface to m responseJson: object & any;ake mocking easy. + * Interface for the HTTP request library used by the wallet. + * + * The request library is bundled into an interface to make mocking and + * request tunneling easy. */ export interface HttpRequestLibrary { + /** + * Make an HTTP GET request. + */ get(url: string, opt?: HttpRequestOptions): Promise; + + /** + * Make an HTTP POST request with a JSON body. + */ postJson( url: string, body: any, @@ -105,18 +123,29 @@ export class BrowserHttpLib implements HttpRequestLibrary { } myRequest.onerror = (e) => { - console.error("http request error"); - reject(Error("could not make XMLHttpRequest")); + logger.error("http request error"); + reject( + OperationFailedError.fromCode( + TalerErrorCode.WALLET_NETWORK_ERROR, + "Could not make request", + { + requestUrl: url, + }, + ), + ); }; myRequest.addEventListener("readystatechange", (e) => { if (myRequest.readyState === XMLHttpRequest.DONE) { if (myRequest.status === 0) { - reject( - Error( - "HTTP Request failed (status code 0, maybe URI scheme is wrong?)", - ), + const exc = OperationFailedError.fromCode( + TalerErrorCode.WALLET_NETWORK_ERROR, + "HTTP request failed (status 0, maybe URI scheme was wrong?)", + { + requestUrl: url, + }, ); + reject(exc); return; } const makeJson = async (): Promise => { @@ -124,10 +153,24 @@ export class BrowserHttpLib implements HttpRequestLibrary { try { responseJson = JSON.parse(myRequest.responseText); } catch (e) { - throw Error("Invalid JSON from HTTP response"); + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "Invalid JSON from HTTP response", + { + requestUrl: url, + httpStatusCode: myRequest.status, + }, + ); } if (responseJson === null || typeof responseJson !== "object") { - throw Error("Invalid JSON from HTTP response"); + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "Invalid JSON from HTTP response", + { + requestUrl: url, + httpStatusCode: myRequest.status, + }, + ); } return responseJson; }; @@ -141,13 +184,14 @@ export class BrowserHttpLib implements HttpRequestLibrary { const parts = line.split(": "); const headerName = parts.shift(); if (!headerName) { - console.error("invalid header"); + logger.warn("skipping invalid header"); return; } const value = parts.join(": "); headerMap.set(headerName, value); }); const resp: HttpResponse = { + requestUrl: url, status: myRequest.status, headers: headerMap, json: makeJson, @@ -165,7 +209,7 @@ export class BrowserHttpLib implements HttpRequestLibrary { postJson( url: string, - body: any, + body: unknown, opt?: HttpRequestOptions, ): Promise { return this.req("post", url, JSON.stringify(body), opt); @@ -176,114 +220,121 @@ export class BrowserHttpLib implements HttpRequestLibrary { } } -export interface PostJsonRequest { - http: HttpRequestLibrary; - url: string; - body: any; - codec: Codec; -} +type TalerErrorResponse = { + code: number; +} & unknown; -/** - * Helper for making Taler-style HTTP POST requests with a JSON payload and response. - */ -export async function httpPostTalerJson( - req: PostJsonRequest, -): Promise { - const resp = await req.http.postJson(req.url, req.body); +type ResponseOrError = + | { isError: false; response: T } + | { isError: true; talerErrorResponse: TalerErrorResponse }; - if (resp.status !== 200) { - let exc: OperationFailedError | undefined = undefined; - try { - const errorJson = await resp.json(); - const m = `received error response (status ${resp.status})`; - exc = new OperationFailedError({ - type: "protocol", - message: m, - details: { - httpStatusCode: resp.status, - errorResponse: errorJson, - }, - }); - } catch (e) { - const m = "could not parse response JSON"; - exc = new OperationFailedError({ - type: "network", - message: m, - details: { - status: resp.status, - }, - }); +export async function readSuccessResponseJsonOrErrorCode( + httpResponse: HttpResponse, + codec: Codec, +): Promise> { + if (!(httpResponse.status >= 200 && httpResponse.status < 300)) { + const errJson = await httpResponse.json(); + const talerErrorCode = errJson.code; + if (typeof talerErrorCode !== "number") { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "Error response did not contain error code", + { + requestUrl: httpResponse.requestUrl, + }, + ), + ); } - throw exc; + return { + isError: true, + talerErrorResponse: errJson, + }; } - let json: any; + const respJson = await httpResponse.json(); + let parsedResponse: T; try { - json = await resp.json(); + parsedResponse = codec.decode(respJson); } catch (e) { - const m = "could not parse response JSON"; - throw new OperationFailedError({ - type: "network", - message: m, - details: { - status: resp.status, + throw OperationFailedError.fromCode( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "Response invalid", + { + requestUrl: httpResponse.requestUrl, + httpStatusCode: httpResponse.status, + validationError: e.toString(), }, - }); + ); } - return req.codec.decode(json); + return { + isError: false, + response: parsedResponse, + }; } - -export interface GetJsonRequest { - http: HttpRequestLibrary; - url: string; - codec: Codec; +export function throwUnexpectedRequestError( + httpResponse: HttpResponse, + talerErrorResponse: TalerErrorResponse, +): never { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + "Unexpected error code in response", + { + requestUrl: httpResponse.requestUrl, + httpStatusCode: httpResponse.status, + errorResponse: talerErrorResponse, + }, + ), + ); } -/** - * Helper for making Taler-style HTTP GET requests with a JSON payload. - */ -export async function httpGetTalerJson( - req: GetJsonRequest, -): Promise { - const resp = await req.http.get(req.url); +export async function readSuccessResponseJsonOrThrow( + httpResponse: HttpResponse, + codec: Codec, +): Promise { + const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec); + if (!r.isError) { + return r.response; + } + throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); +} - if (resp.status !== 200) { - let exc: OperationFailedError | undefined = undefined; - try { - const errorJson = await resp.json(); - const m = `received error response (status ${resp.status})`; - exc = new OperationFailedError({ - type: "protocol", - message: m, - details: { - httpStatusCode: resp.status, - errorResponse: errorJson, - }, - }); - } catch (e) { - const m = "could not parse response JSON"; - exc = new OperationFailedError({ - type: "network", - message: m, - details: { - status: resp.status, - }, - }); +export async function readSuccessResponseTextOrErrorCode( + httpResponse: HttpResponse, +): Promise> { + if (!(httpResponse.status >= 200 && httpResponse.status < 300)) { + const errJson = await httpResponse.json(); + const talerErrorCode = errJson.code; + if (typeof talerErrorCode !== "number") { + throw new OperationFailedError( + makeErrorDetails( + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + "Error response did not contain error code", + { + requestUrl: httpResponse.requestUrl, + }, + ), + ); } - throw exc; + return { + isError: true, + talerErrorResponse: errJson, + }; } - let json: any; - try { - json = await resp.json(); - } catch (e) { - const m = "could not parse response JSON"; - throw new OperationFailedError({ - type: "network", - message: m, - details: { - status: resp.status, - }, - }); + const respJson = await httpResponse.text(); + return { + isError: false, + response: respJson, + }; +} + +export async function readSuccessResponseTextOrThrow( + httpResponse: HttpResponse, +): Promise { + const r = await readSuccessResponseTextOrErrorCode(httpResponse); + if (!r.isError) { + return r.response; } - return req.codec.decode(json); + throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); } -- cgit v1.2.3