Skip to content
Closed
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
231 changes: 135 additions & 96 deletions packages/client/.aegir.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,135 @@
import EchoServer from 'aegir/echo-server'
import body from 'body-parser'

/** @type {import('aegir').PartialOptions} */
const options = {
test: {
before: async () => {
let callCount = 0
let lastCalledUrl = ''
const providers = new Map()
const peers = new Map()
const ipnsGet = new Map()
const ipnsPut = new Map()
const echo = new EchoServer()
echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'}))
echo.polka.use(body.text())
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()
})
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)
})
echo.polka.post('/add-peers/:peerId', (req, res) => {
callCount++
peers.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/peers/:peerId', (req, res) => {
callCount++
const records = peers.get(req.params.peerId) ?? '[]'
peers.delete(req.params.peerId)

res.end(records)
})
echo.polka.post('/add-ipns/:peerId', (req, res) => {
callCount++
ipnsGet.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
const record = ipnsGet.get(req.params.peerId) ?? ''
ipnsGet.delete(req.params.peerId)

res.end(record)
})
echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
ipnsPut.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/get-ipns/:peerId', (req, res) => {
callCount++
const record = ipnsPut.get(req.params.peerId) ?? ''
ipnsPut.delete(req.params.peerId)

res.end(record)
})
echo.polka.get('/get-call-count', (req, res) => {
res.end(callCount.toString())
})
echo.polka.get('/reset-call-count', (req, res) => {
callCount = 0
res.end()
})
echo.polka.get('/last-called-url', (req, res) => {
res.end(lastCalledUrl)
})

await echo.start()

return {
env: {
ECHO_SERVER: `http://${echo.host}:${echo.port}`
},
echo
}
},
after: async (_, beforeResult) => {
if (beforeResult.echo != null) {
await beforeResult.echo.stop()
}
}
}
}

export default options
import EchoServer from 'aegir/echo-server'
import body from 'body-parser'

/** @type {import('aegir').PartialOptions} */
const options = {
test: {
before: async () => {
let callCount = 0
let lastCalledUrl = ''
const providers = new Map()
const peers = new Map()
const ipnsGet = new Map()
const ipnsPut = new Map()
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++
try {
if (!req.headers['content-type']?.includes('application/json')) {
res.statusCode = 400
res.end(JSON.stringify({
error: 'Invalid content type. Expected application/json',
code: 'ERR_INVALID_INPUT'
}))
providers.delete(req.params.cid)
return
}

const data = typeof req.body === 'string'
? { Providers: req.body.split('\n').map(line => JSON.parse(line)) }
: 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++
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++
peers.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/peers/:peerId', (req, res) => {
callCount++
const records = peers.get(req.params.peerId) ?? '[]'
peers.delete(req.params.peerId)

res.end(records)
})
echo.polka.post('/add-ipns/:peerId', (req, res) => {
callCount++
ipnsGet.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
const record = ipnsGet.get(req.params.peerId) ?? ''
ipnsGet.delete(req.params.peerId)

res.end(record)
})
echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => {
callCount++
ipnsPut.set(req.params.peerId, req.body)
res.end()
})
echo.polka.get('/get-ipns/:peerId', (req, res) => {
callCount++
const record = ipnsPut.get(req.params.peerId) ?? ''
ipnsPut.delete(req.params.peerId)

res.end(record)
})
echo.polka.get('/get-call-count', (req, res) => {
res.end(callCount.toString())
})
echo.polka.get('/reset-call-count', (req, res) => {
callCount = 0
res.end()
})
echo.polka.get('/last-called-url', (req, res) => {
res.end(lastCalledUrl)
})

await echo.start()

return {
env: {
ECHO_SERVER: `http://${echo.host}:${echo.port}`
},
echo
}
},
after: async (_, beforeResult) => {
if (beforeResult.echo != null) {
await beforeResult.echo.stop()
}
}
}
}

export default options
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
29 changes: 20 additions & 9 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,34 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
this.#addFilterParams(url, options.filterAddrs, options.filterProtocols)
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 +157,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 Expand Up @@ -387,7 +398,7 @@ export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV
// Only try to use cache for GET requests
if (requestMethod === 'GET') {
const cachedResponse = await this.cache?.match(url)
if (cachedResponse != null) {
if (cachedResponse?.ok === true) {
// Check if the cached response has expired
const expires = parseInt(cachedResponse.headers.get('x-cache-expires') ?? '0', 10)
if (expires > Date.now()) {
Expand Down
Loading