summaryrefslogtreecommitdiff
path: root/src/util/http.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/http.ts')
-rw-r--r--src/util/http.ts267
1 files changed, 159 insertions, 108 deletions
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 <http://www.gnu.org/licenses/>
*/
-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<any>;
@@ -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<HttpResponse>;
+
+ /**
+ * 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<any> => {
@@ -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<HttpResponse> {
return this.req("post", url, JSON.stringify(body), opt);
@@ -176,114 +220,121 @@ export class BrowserHttpLib implements HttpRequestLibrary {
}
}
-export interface PostJsonRequest<RespType> {
- http: HttpRequestLibrary;
- url: string;
- body: any;
- codec: Codec<RespType>;
-}
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
-/**
- * Helper for making Taler-style HTTP POST requests with a JSON payload and response.
- */
-export async function httpPostTalerJson<RespType>(
- req: PostJsonRequest<RespType>,
-): Promise<RespType> {
- const resp = await req.http.postJson(req.url, req.body);
+type ResponseOrError<T> =
+ | { 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<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ 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<RespType> {
- http: HttpRequestLibrary;
- url: string;
- codec: Codec<RespType>;
+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<RespType>(
- req: GetJsonRequest<RespType>,
-): Promise<RespType> {
- const resp = await req.http.get(req.url);
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ 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<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ 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<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
}
- return req.codec.decode(json);
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
}