Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/maticjs-abi-fetch-error-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@maticnetwork/maticjs': patch
---

Surface the real cause when network/ABI metadata fails to load. `HttpRequest` now checks `res.ok` and reports the HTTP status plus a body snippet (and flags non-JSON responses) instead of letting `res.json()` throw a context-free parse error, and `Web3SideChainClient.init` preserves that underlying error — as the message and as `cause` — rather than discarding it and rethrowing a bare `network <x> - <v> is not supported`. A transport or CDN failure (non-2xx, HTML error/challenge page, timeout) is now diagnosable instead of masquerading as an unsupported network.
27 changes: 21 additions & 6 deletions packages/maticjs/src/utils/http_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ const fetch: (input: RequestInfo, init?: RequestInit) => Promise<Response> = (()
return window.fetch;
})();

// Fail with the HTTP status / body on a non-2xx or non-JSON response instead of
// letting a bare `res.json()` throw a context-free parse error.
async function parseJsonResponse<T>(res: Response, method: string, url: string): Promise<T> {
const text = await res.text();
if (!res.ok) {
const snippet = text.slice(0, 200);
throw new Error(`HTTP ${res.status} ${res.statusText} for ${method} ${url}: ${snippet}`);
}
try {
return JSON.parse(text) as T;
} catch {
const contentType = res.headers.get('content-type') ?? 'unknown';
const snippet = text.slice(0, 200);
throw new Error(
`Expected JSON from ${method} ${url} (content-type: ${contentType}) but parsing failed: ${snippet}`
);
}
}

export class HttpRequest {
baseUrl = '';

Expand All @@ -30,9 +49,7 @@ export class HttpRequest {
'Content-Type': 'application/json',
Accept: 'application/json'
}
}).then((res) => {
return res.json();
});
}).then((res) => parseJsonResponse<T>(res, 'GET', fullUrl));
}

post(url = '', body) {
Expand All @@ -45,8 +62,6 @@ export class HttpRequest {
Accept: 'application/json'
},
body: body ? JSON.stringify(body) : null
}).then((res) => {
return res.json();
});
}).then((res) => parseJsonResponse(res, 'POST', fullUrl));
}
}
11 changes: 9 additions & 2 deletions packages/maticjs/src/utils/web3_side_chain_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,15 @@ export class Web3SideChainClient<T_CONFIG> {
const version = normalizedConfig.version;
const abiManager = (this.abiManager = new ABIManager(network, version));
this.logger.log('init called', abiManager);
return abiManager.init().catch(() => {
throw new Error(`network ${network} - ${version} is not supported`);
return abiManager.init().catch((err) => {
// Preserve the underlying fetch/parse failure instead of discarding it.
const reason = err instanceof Error ? err.message : String(err);
// `cause` via assignment: the Error(message, { cause }) overload postdates
// this package's TS lib target.
throw Object.assign(
new Error(`network ${network} - ${version} is not supported: ${reason}`),
{ cause: err }
);
});
}

Expand Down
79 changes: 79 additions & 0 deletions packages/maticjs/tests/http-request-error-visibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { Server } from 'node:http';

/**
* Regression tests for HTTP error visibility in HttpRequest.
*
* Background
* ----------
* `ABIManager.init()` resolves network metadata by GETting
* `<abiStoreUrl>/<network>/<version>/index.json`. The previous `HttpRequest.get`
* did a bare `res.json()` with no status check, so:
*
* - a non-2xx response (e.g. a CDN 403/429) made `res.json()` throw a
* context-free "Unexpected token <" — the status was lost; and
* - `Web3SideChainClient.init` then *discarded* that error entirely and
* rethrew the generic "network X - vY is not supported".
*
* The net effect was that any transport/CDN problem was undiagnosable. These
* tests pin the fixed behaviour: the status and a body snippet survive.
*/
import { createServer } from 'node:http';

import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { HttpRequest } from '../src/utils/http_request';

let server: Server;
let baseUrl: string;
// Per-request knobs the test server reads to shape its response.
let nextStatus = 200;
let nextContentType = 'application/json';
let nextBody = '{}';

beforeAll(async () => {
server = createServer((_req, res) => {
res.writeHead(nextStatus, { 'content-type': nextContentType });
res.end(nextBody);
});
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
const addr = server.address();
if (addr === null || typeof addr === 'string') throw new Error('failed to bind test server');
baseUrl = `http://127.0.0.1:${addr.port}/`;
});

afterAll(async () => {
await new Promise<void>((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve()))
);
});

describe('HttpRequest.get error visibility', () => {
it('surfaces the HTTP status and body on a non-2xx response', async () => {
nextStatus = 403;
nextContentType = 'text/html';
nextBody = '<html><body>Forbidden by edge</body></html>';

await expect(new HttpRequest(baseUrl).get('index.json')).rejects.toThrow(
/HTTP 403[\s\S]*Forbidden by edge/
);
});

it('reports a non-JSON 2xx body instead of a context-free parse error', async () => {
nextStatus = 200;
nextContentType = 'text/html';
nextBody = '<html><body>rate limited</body></html>';

await expect(new HttpRequest(baseUrl).get('index.json')).rejects.toThrow(
/Expected JSON[\s\S]*content-type: text\/html[\s\S]*rate limited/
);
});

it('still returns parsed JSON on a normal 2xx response', async () => {
nextStatus = 200;
nextContentType = 'application/json';
nextBody = JSON.stringify({ Main: { Contracts: {} } });

const result = await new HttpRequest(baseUrl).get<{ Main: unknown }>('index.json');
expect(result).toHaveProperty('Main');
});
});
Loading