-
Notifications
You must be signed in to change notification settings - Fork 106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: content-type response header hints how to process response #426
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly believe we should not leak custom types onto object returned by .json()
. See https://github.com/ipfs/helia/pull/426/files#r1485686844
The extensive stubbing in the `@helia/verified-fetch` tests have some baked-in assumptions about how the codecs work which are not easy to unpick. It's quick to test using the actual codecs if the block data is already present so remove the stubs and use a network-less Helia node to make the tests more reliable.
0a51386
to
6eeb44d
Compare
215c5f8
to
57f7345
Compare
DAG-JSON and DAG-CBOR can have embeded CIDs which are deserialized to CID objects by the `@ipld/dag-json` and `@ipld/dag-cbor` modules. This fixes a bug whereby we were JSON.stringifying them which lead to rubbish being returned from `.json()`.
57f7345
to
95e5618
Compare
I agree, though we now have a weird DX niggle where these return incompatible types even though you are interacting with the same ecosystem of modules: import { verifiedFetch } from '@helia/verified-fetch'
import { dagJson } from '@helia/dag-json'
const cid = CID.parse('QmDAG-JSONwithCID')
// obj has CID as `{ "/": "QmFoo" }`
const res = await verifiedFetch(`ipfs://${cid}`)
const obj = await res.json()
// obj has CID as object
const d = dagJson(helia)
const obj = await d.get(CID.parse('QmDAG-JSONwithCID')
const obj = {
hello: 'world',
bigInt: 12345n,
bytes: Uint8Array.from([0, 1, 2, 3, 4])
}
const d = dagCbor(helia)
const cid = await d.add(obj)
const res = await verifiedFetch(`ipfs://${cid}`)
const output = await res.json() // 💥 I guess the thing to do here is to convert We could add a It wouldn't survive caching, but we can't cache It'd be nice to have |
With verified-fetch in a SW (helia-service-worker-gateway), the returned responses are cached because its the Response object itself that's cached by the service-worker, based on requested URL. I'm not 100% sure of the intricacies there, but the returned object will be whatever is in the body i'm sure, so we need to make sure the stream/arrayBuffer given to the This also brings up a few targeted use cases:
I think having Maybe we could provide some interface for verifiedFetch similar to |
I think getting DAG-CBOR experience with $ echo -n '{"hello":"world"}' | ipfs dag put # [--store-codec=dag-cbor] [--input-codec=dag-json]
bafyreidykglsfhoixmivffc5uwhcgshx4j465xwqntbmu43nb2dzqwfvae So if someone builds an app, and wants to read that dag-cbor back with verified-fetch, they would want > resp.json()
{"hello":"world"} For practical purposes the roundtrip (json→cbor→json) works fine, as long it is limited to the safe DAG- subset of CBOR and JSON, listed in
I think if we have to choose between @achingbrain Is my assumption correct that if someone wants to get raw |
Yes, they can request the raw block and decode it themselves, then they can use the rich types and we don't have to worry about breaking Notes from the dApps working group call: We can try to deserialize the CBOR as JSON and error if it fails. The user can then re-request with the header Presumably if they request with |
That said, what if we just try to decode and just let the (familiar) content type of the response dictate how it can be consumed, to save the user making two requests and so they don't have to care about If the block decodes as JSON-compliant DAG-CBOR it's import { verifiedFetch } from '@helia/verified-fetch'
import * as dagCbor from '@ipld/dag-cbor'
const response = await verifiedFetch('ipfs://bafyDagCbor')
let obj
if (response.headers.get('Content-Type')?.includes('application/json')) {
obj = await response.json()
} else {
obj = dagCbor.decode(await response.arrayBuffer())
} vs import { verifiedFetch } from '@helia/verified-fetch'
import * as dagCbor from '@ipld/dag-cbor'
let response = await verifiedFetch('ipfs://bafyDagCbor')
let obj
try {
obj = await response.json() // 💥
} catch (err) {
if (!err.message.includes('some JSON error') {
throw err
}
// try again
response = await verifiedFetch('ipfs://bafyDagCbor', {
headers: {
accept: 'application/vnd.ipld.raw'
}
})
try {
obj = dagCbor.decode(await response.arrayBuffer())
} catch (err) {
console.error('giving up', err)
throw err
}
} I think I prefer the version without exceptions-as-control-flow. |
Ok, I think we're in a good place now. JSON codecimport { verifiedFetch } from '@helia/verified-fetch'
import * as json from 'multiformats/codecs/json'
const res = await verifiedFetch('ipfs://bafyJSONCodec')
// get object representation of JSON body
console.info(await res.json()) // { ... }
// alternative way to do the same thing
const obj = json.decode(new Uint8Array(await res.arrayBuffer()))
console.info(obj) // { ... } DAG-JSON codecimport { verifiedFetch } from '@helia/verified-fetch'
import * as dagJson from '@ipld/dag-json'
const res = await verifiedFetch('ipfs://bafyDAGJSONCodec')
// get spec-compliant plain-old-JSON object that happens to contain a CID
console.info(await res.json()) // { cid: { '/': 'Qmfoo' } }
// ...or use @ipld/dag-json to get rich-object version
const obj = dagJson.decode(new Uint8Array(await res.arrayBuffer()))
console.info(obj) // { cid: CID('Qmfoo') } DAG-CBOR codecimport { verifiedFetch } from '@helia/verified-fetch'
import * as dagCbor from '@ipld/dag-cbor'
const res = await verifiedFetch('ipfs://bafyDAGCBORCodec')
if (res.headers.get('content-type') === 'application/json') {
// CBOR was JSON-friendly
console.info(await res.json()) // { ... }
} else {
// CBOR was not JSON-friendly, must use @ipld/dag-cbor to decode
const obj = dagCbor.decode(new Uint8Array(await res.arrayBuffer()))
console.info(obj) // { ... }
} If the I've added README docs explaining the above, please comment if they muddy the waters. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved, but I imagine we'll wait on multiformats/js-multiformats#287. Looks amazing, fantastic work as always.
* | ||
* [DAG-CBOR](https://ipld.io/docs/codecs/known/dag-cbor/) uses the [Concise Binary Object Representation](https://cbor.io/) format for serialization instead of JSON. | ||
* | ||
* This supports more datatypes in a safer way than JSON and is smaller on the wire to boot so is usually preferable to JSON or DAG-JSON. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
private readonly pathWalker: PathWalkerFn | ||
private readonly log: Logger | ||
private readonly contentTypeParser: ContentTypeParser | undefined | ||
|
||
constructor ({ helia, ipns, unixfs, dagJson, json, dagCbor, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) { | ||
constructor ({ helia, ipns, unixfs, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great. The more we can stay out of pulling in all of the extra codecs, the better.
// N.b. this is not possible because the incoming block is turned into JSON | ||
// and returned as the response body, so `.arrayBufer()` returns a string | ||
// encoded into a Uint8Array which we can't parse as CBOR | ||
it.skip('can round trip JSON-compliant dag-cbor via .arrayBuffer()', async () => { | ||
const obj = { | ||
hello: 'world' | ||
} | ||
const c = dagCbor(helia) | ||
const cid = await c.add(obj) | ||
|
||
const resp = await verifiedFetch.fetch(cid) | ||
expect(resp.headers.get('content-type')).to.equal('application/json') | ||
|
||
const output = ipldDagCbor.decode(new Uint8Array(await resp.arrayBuffer())) | ||
await expect(c.add(output)).to.eventually.deep.equal(cid) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to call this out in packageDocumentation (or is it already?)
Is this forever not-possible, or something we hope to support in the future? (we could remove this test, or assert that it isn't equal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this in because it's the logical next test give the input/output matrix, but it won't work for reasons outlined in the comment. We can remove it if we think it's redundant.
I opened a separate PR with some small docs refinements #436 The one challenge here, which isn't a blocker to me, is that for projects that use dag-cbor extensively, they might have some CIDs that can roundtrip to json and some that don't (with BigInts and Uint8Arrays). In its current state, there's no way to opt out of the automatic JSON parsing, so depending on their data, they either have to pay the cost of attempting to parse as json or deal with the difference cases (json/binary) in their code. |
If they know it's going to fail they can add an E.g.: import { verifiedFetch } from '@helia/verified-fetch'
import * as dagCbor from '@ipld/dag-cbor'
const res = await verifiedFetch('ipfs://bafyDAGCBORCodec', {
headers: {
accept: 'application/vnd.ipld.raw'
}
})
const obj = dagCbor.decode(new Uint8Array(await res.arrayBuffer()))
console.info(obj) // { ... } |
From a look at the code, this isn't implemented and the example you provided returns a response object with status 501. helia/packages/verified-fetch/test/verified-fetch.spec.ts Lines 98 to 113 in f2c0f35
Another thing which might be a bug is that we pass the options object to a method which expects the body as the first parameter: helia/packages/verified-fetch/src/verified-fetch.ts Lines 68 to 73 in f2c0f35
I also tried the example and it doesn't work |
Co-authored-by: Daniel N <2color@users.noreply.github.com>
Co-authored-by: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com>
I'm going to merge this so we can proceed with the tests for the different data types in the examples. |
Description
DAG-JSON and DAG-CBOR can have embeded CIDs which are deserialized to CID objects by the
@ipld/dag-json
and@ipld/dag-cbor
modules.This fixes a bug whereby we were JSON.stringifying them which lead to rubbish being returned from
.json()
.Notes & open questions
This overwrites the
.json
method on the returnedResponse
object. Is that bad?Change checklist