diff --git a/packages/client/.aegir.js b/packages/client/.aegir.js index 14e7890..1aeb139 100644 --- a/packages/client/.aegir.js +++ b/packages/client/.aegir.js @@ -14,20 +14,55 @@ const options = { const echo = new EchoServer() echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'})) echo.polka.use(body.text()) + echo.polka.use(body.json()) echo.polka.use((req, res, next) => { next() lastCalledUrl = req.url }) echo.polka.post('/add-providers/:cid', (req, res) => { callCount++ - providers.set(req.params.cid, req.body) - res.end() + try { + + let data + try { + // when passed data from a test where `body=providers.map(prov => JSON.stringify(prov)).join('\n')` + data = { Providers: req.body.split('\n').map(line => JSON.parse(line)) } + } catch (err) { + // when passed data from a test where `body=JSON.stringify({ Providers: providers })` + data = req.body + } + + providers.set(req.params.cid, data) + res.end(JSON.stringify({ success: true })) + } catch (err) { + console.error('Error in add-providers:', err) + res.statusCode = 400 + res.end(JSON.stringify({ + error: err.message, + code: 'ERR_INVALID_INPUT' + })) + providers.delete(req.params.cid) + } }) echo.polka.get('/routing/v1/providers/:cid', (req, res) => { callCount++ - const records = providers.get(req.params.cid) ?? '[]' - providers.delete(req.params.cid) - res.end(records) + try { + const providerData = providers.get(req.params.cid) || { Providers: [] } + const acceptHeader = req.headers.accept + + if (acceptHeader?.includes('application/x-ndjson')) { + res.setHeader('Content-Type', 'application/x-ndjson') + const providers = Array.isArray(providerData.Providers) ? providerData.Providers : [] + res.end(providers.map(p => JSON.stringify(p)).join('\n')) + } else { + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(providerData)) + } + } catch (err) { + console.error('Error in get providers:', err) + res.statusCode = 500 + res.end(JSON.stringify({ error: err.message })) + } }) echo.polka.post('/add-peers/:peerId', (req, res) => { callCount++ diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index bc44672..83b767b 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -122,16 +122,22 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal } const res = await this.#makeRequest(url.toString(), getOptions) - if (res.status === 404) { + if (res == null) { + throw new BadResponseError('No response received') + } + if (!res.ok) { + if (res.status === 404) { // https://specs.ipfs.tech/routing/http-routing-v1/#response-status-codes // 404 (Not Found): must be returned if no matching records are found - throw new NotFoundError('No matching records found') - } + throw new NotFoundError('No matching records found') + } - if (res.status === 422) { + if (res.status === 422) { // https://specs.ipfs.tech/routing/http-routing-v1/#response-status-codes // 422 (Unprocessable Entity): request does not conform to schema or semantic constraints - throw new InvalidRequestError('Request does not conform to schema or semantic constraints') + throw new InvalidRequestError('Request does not conform to schema or semantic constraints') + } + throw new BadResponseError(`Unexpected status code: ${res.status}`) } if (res.body == null) { @@ -139,7 +145,11 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV } const contentType = res.headers.get('Content-Type') - if (contentType === 'application/json') { + if (contentType == null) { + throw new BadResponseError('No Content-Type header received') + } + + if (contentType?.startsWith('application/json')) { const body = await res.json() for (const provider of body.Providers) { @@ -148,13 +158,15 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV yield record } } - } else { + } else if (contentType.includes('application/x-ndjson')) { for await (const provider of ndjson(toIt(res.body))) { const record = this.#conformToPeerSchema(provider) if (record != null) { yield record } } + } else { + throw new BadResponseError(`Unsupported Content-Type: ${contentType}`) } } catch (err) { log.error('getProviders errored:', err) diff --git a/packages/client/test/index.spec.ts b/packages/client/test/index.spec.ts index 1ffff14..cae1397 100644 --- a/packages/client/test/index.spec.ts +++ b/packages/client/test/index.spec.ts @@ -65,6 +65,41 @@ describe('delegated-routing-v1-http-api-client', () => { }))) }) + it('should handle different Content-Type headers for JSON responses', async () => { + const providers = [{ + Protocol: 'transport-bitswap', + Schema: 'bitswap', + Metadata: 'gBI=', + ID: (await generateKeyPair('Ed25519')).publicKey.toString(), + Addrs: ['/ip4/41.41.41.41/tcp/1234'] + }] + + const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + const contentTypes = [ + 'application/json', + 'application/json; charset=utf-8', + 'application/json;charset=UTF-8' + ] + + for (const contentType of contentTypes) { + // Add providers with proper payload structure + await fetch(`${process.env.ECHO_SERVER}/add-providers/${cid.toString()}`, { + method: 'POST', + headers: { + 'Content-Type': contentType + }, + body: JSON.stringify({ Providers: providers }) + }) + + await new Promise((resolve) => setTimeout(resolve, 100)) + const provs = await all(client.getProviders(cid)) + + expect(provs).to.have.lengthOf(1, `Failed for Content-Type: ${contentType}`) + expect(provs[0].ID.toString()).to.equal(providers[0].ID) + expect(provs[0].Addrs[0].toString()).to.equal(providers[0].Addrs[0]) + } + }) + it('should add filter parameters the query of the request url', async () => { const providers = [{ Protocol: 'transport-bitswap',