Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
45 changes: 40 additions & 5 deletions packages/client/.aegir.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand Down
1 change: 1 addition & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"@libp2p/logger": "^5.0.1",
"@libp2p/peer-id": "^5.0.1",
"@multiformats/multiaddr": "^12.3.1",
"@playwright/test": "^1.50.1",
"any-signal": "^4.1.1",
"browser-readablestream-to-it": "^2.0.7",
"ipns": "^10.0.0",
Expand Down
26 changes: 19 additions & 7 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,34 @@ 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) {
throw new BadResponseError('Routing response had no body')
}

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) {
Expand All @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions packages/client/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading