Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export async function doHttpRequest(
if (typeof (errBody) === "object" && 'errcode' in errBody) {
const redactedBody = respIsBuffer ? '<Buffer>' : redactObjectForLogging(errBody);
LogService.error("MatrixHttpClient", "(REQ-" + requestId + ")", redactedBody);
throw new MatrixError(errBody, response.statusCode);
throw new MatrixError(errBody, response.statusCode, response.headers);
}

// Don't log the body unless we're in debug mode. They can be large.
Expand Down
34 changes: 32 additions & 2 deletions src/models/MatrixError.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import { LogService } from "../logging/LogService";

/**
* Represents an HTTP error from the Matrix server.
* @category Error handling
*/
export class MatrixError extends Error {
/**
* Parse a Retry-After header into a number of milliseconds.
* @see https://www.rfc-editor.org/rfc/rfc9110#field.retry-after
* @param header The value of a Retry-After header.
* @throws If the date could not be parsed.
*/
static parseRetryAfterHeader(header: string): number {
// First try to parse as seconds
const retryAfterSeconds = parseInt(header, 10);
if (!Number.isNaN(retryAfterSeconds)) {
return retryAfterSeconds * 1000;
}
const retryAfterDate = new Date(header);
return retryAfterDate.getTime() - Date.now();
}

/**
* The Matrix error code
*/
Expand All @@ -23,11 +41,23 @@ export class MatrixError extends Error {
* @param body The error body.
* @param statusCode The HTTP status code.
*/
constructor(public readonly body: { errcode: string, error: string, retry_after_ms?: number }, public readonly statusCode: number) {
constructor(public readonly body: { errcode: string, error: string, retry_after_ms?: number }, public readonly statusCode: number, headers: Record<string, string>) {
super();
this.errcode = body.errcode;
this.error = body.error;
this.retryAfterMs = body.retry_after_ms;
const retryAfterHeader = headers['retry-after'];
if (this.statusCode === 429 && retryAfterHeader) {
try {
this.retryAfterMs = MatrixError.parseRetryAfterHeader(retryAfterHeader);
} catch (ex) {
// Could not parse...skip handling for now.
LogService.warn("MatrixError", "Could not parse Retry-After header from request.", ex);
}
}
// Fall back to the deprecated retry_after_ms property.
if (!this.retryAfterMs && body.retry_after_ms) {
this.retryAfterMs = body.retry_after_ms;
}
}

/**
Expand Down
39 changes: 39 additions & 0 deletions test/models/MatrixErrorTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MatrixError } from "../../src";

describe("MatrixError", () => {
it("should construct a basic MatrixError", () => {
const err = new MatrixError({ 'errcode': 'M_TEST', "error": 'Test fail' }, 500, {});
expect(err.message).toBe('M_TEST: Test fail');
expect(err.retryAfterMs).toBeUndefined();
});
it("should handle a 429 without a retry duration", () => {
const err = new MatrixError({ 'errcode': 'M_TEST', "error": 'Test fail' }, 429, {});
expect(err.message).toBe('M_TEST: Test fail');
expect(err.retryAfterMs).toBeUndefined();
});
it("should handle a 429 with a Retry-After header (time)", () => {
// Should ignore the deprecated field.
const err = new MatrixError({ 'errcode': 'M_TEST', "error": 'Test fail', "retry_after_ms": 5 }, 429, {
'retry-after': '10',
});
expect(err.message).toBe('M_TEST: Test fail');
expect(err.retryAfterMs).toEqual(10000);
});
it("should handle a 429 with a Retry-After header (date)", () => {
jest
.spyOn(global.Date, 'now')
.mockImplementationOnce(() => new Date('Wed, 20 Mar 2024 10:18:16 UTC').valueOf());

// Should ignore the deprecated field.
const err = new MatrixError({ 'errcode': 'M_TEST', "error": 'Test fail', "retry_after_ms": 5 }, 429, {
'retry-after': 'Wed, 20 Mar 2024 10:18:26 UTC',
});
expect(err.message).toBe('M_TEST: Test fail');
expect(err.retryAfterMs).toEqual(10000);
});
it("should handle a 429 with a Retry-After header (date)", () => {
const err = new MatrixError({ 'errcode': 'M_TEST', "error": 'Test fail', "retry_after_ms": 5 }, 429, {});
expect(err.message).toBe('M_TEST: Test fail');
expect(err.retryAfterMs).toEqual(5);
});
});