-
Notifications
You must be signed in to change notification settings - Fork 60
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
[RFC] Add Appendix A: Persisted Documents #264
base: main
Are you sure you want to change the base?
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 am super excited to see this come to fruition! Should we remove the RFC?
|
||
The {operationName} parameter, if present, must be a string. | ||
|
||
Each of the {variables} and {extensions} parameters, if used, MUST be encoded as |
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 have to define expectations in case this exceeds maximum URL size?
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.
If we do; we should do it in the main spec: https://graphql.github.io/graphql-over-http/draft/#sec-GET
The Appendix tries not to redundantly repeat statements from the main spec if it can avoid it.
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.
Which reminds me; I heard that some people are using headers to specify variables when using GraphQL-over-GET... Apparently that works around the length limit 🤨
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.
Yes, a lot of folks use headers and then the server adds them to the Vary
response-headers so browsers/... can know that it is part of the cache-key
c08b7a1
to
7863941
Compare
I think the RFC document still warrants discussion; let's leave it until after the meeting. |
Added to October 26th agenda: https://github.com/graphql/graphql-over-http/blob/main/working-group/agendas/2023/2023-10-26.md |
|
||
The server should retrieve the GraphQL Document identified by the {documentId} | ||
parameter. If the server fails to retrieve the document, it MUST respond with a | ||
well-formed _GraphQL response_ consisting of a single error. Otherwise, it |
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 suggest we define the contents of the error sufficiently for the client to conclusively recognize.
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.
There's not much scope to do that currently; the best that we can do is ensure that the error message starts with, ends with, or contains, a particular string. Error codes would need to be specified in the main GraphQL specification for us to use them here, and writing to extensions
from an official specification has all the issues previously raised. Since this is only needed for APQ currently, I'm happy leaving it unspecified (and addressing it when APQ is added), but I would support the addition of a non-normative example of the error message.
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.
+1 to non-normative example. Attempt:
Note: typically, the error allows recognizing failures to retrieve a document:
\```json
{
"errors": [{
"message: "unknown document identifier"
}]
}
\```
Implementation may add a dedicated error code in the response extensions as described in [the error result format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
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.
Was coming to ask if there was a consistent error when the document identifier is not found
:: A _persisted document request_ is an HTTP request that encodes the following | ||
parameters in one of the manners described in this specification: | ||
|
||
- {documentId} - (_Required_, string): The string identifier for the Document. |
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.
query
is omitted here and throughout. Makes sense, as it is not necessary for operation and should not be included. But I think APQ-type behavior should be considered and allowed for within the spec. Preferably, it is an optional feature as part of this spec, or otherwise include a note to the effect that query
may be allowed in certain use cases, etc.
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.
In an unofficial APQ request based on this specification, the query should go into extensions
, and the error code used to detect the missing query should also go into the error's extensions
. We may specify query
in future if we officially specify something like APQ; that's definitely feasible over what we already have.
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.
What are your opinions on providing the documentId
via the URL /graphql/<documentId>
?
One of the benefits would be easier debugging and visibility in dev tooling.
Edit: already covered via https://github.com/graphql/graphql-over-http/pull/264/files#diff-9be5577e05ae2112d2b8f95584b162d0dec01453bf6c85df58bf5db4f2c9727aR166-R168
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.
Yes I think this should be encouraged more; it's great for caching. I'd welcome your edits to address this, if you were so inclined.
sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b | ||
``` | ||
|
||
### Custom Document Identifier |
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.
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 think it would be unwise to recommend that people do this based solely on the operation name (the risk of clashes as they iterate their queries is too high); however I would support the name being factored into the document identifier along with some hashing; e.g. something like: createHash('sha256').update(documentSource).digest('hex').substring(0, 12) + '_' + operationName
. The operationName
query parameter is already specified; perhaps we should add a non-normative note recommending that clients include it to aid in debugging?
identification methods by ensuring that the prefix starts `x-`; otherwise, all | ||
prefixes are reserved for reasons of future expansion. | ||
|
||
### SHA256 Hex Document Identifier |
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 wonder if actual encryption is needed? what's the benefit of having sha256
over any other opaque string? 🤔
We can allow users how to encode/map their persisted documents, and then do matching based on that value?
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.
The aim of a specification like this is interoperability; so a client that supports persisted operations should be able to use a server that supports persisted operations without too much additional setup. Sharing details of the document identification method used out-of-band is supported (explicitly by x-
prefix, or by custom identifiers); but for maximum compatibility there should be a shared baseline in my opinion.
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 expect that the benefit is mainly for APQ support, where a client can enable APQ without knowledge of the server implementation, and so a consistent implementation is essential. SHA256 can be computed natively by modern browsers with a very low collision rate, making it a good choice in this scenario.
However, in a scenario where the identifiers are only known to the server, and must be registered with the server in order for the client to operate (as so far is documented in this RFC), they might as well be any opaque key returned by the query storage database. If the database stores the queries with an auto-incrementing integer as an identifier, that would work just as well.
Even so, there are still some benefits to using a hash:
- Should the query storage mechanism change, the identifiers will remain constant (but the same would be true of a GUID)
- Inherit deduplication for stored queries
- Enhanced security, as attackers can't easily guess valid identifiers, reducing the risk of unauthorized query execution
I'm fine with the current suggestion of sha256:
but allowing for x-id:
and similar.
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.
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.
@dotansimha Indeed, that's already allowed under this spec. The issue is it requires coordination between server and client (they need to agree on how operation-1
is derived). For maximum compatibility, if both server and client already know how to identify operations (e.g. standardized SHA256 hash) then all that's left is to transfer the docs from client to server, which can happen after the client has been built, rather than before or during, and no configuration is required on server or client easing adoption for everyone and encouraging more people to use an operation allowlist.
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.
Oh, right, but the queries still need to be transferred to the server is what you're saying. Yes, that's true, but it can be done after the client is built (but before it's deployed). That's different from having to do it during the build process; it means that clients can be built and persisted documents written even before the server exists. It also allows for arbitrary transfer of documents to the server (you can send them one-way on a pen drive through the mail if you want!).
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.
So the client and the server must be coordinated, and we recommend to use SHA256, right?
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.
"Recommend" and "should" are the same according to RFC2119, so I think that is saying what's already there. If the server follows this recommendation, the client doesn't need any configuration. If the server doesn't do this; then you need to coordinate between client and server. For optimal interoperability, no coordination should be necessary.
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.
To be pedantic there, I'd argue that sharing the "sha256" method is still coordination between the client and server. Plus the documents need to be actually transferred (on a pen drive or avian carrier!).
So coordination is always required but sha256 is a convenient and widespread default which we recommend (hence the should)?
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.
Indeed, it is a very light-touch asynchronous form of coordination. Essentially the coordination boils down to two things: 1. does the server support SHA256 hashes (don't necessarily need to ask the server this, it's a fact that should be established in the development team); 2. we need a way to send the operations+hashes to the server and to know when they have been persisted.
Client is informed that server supports SHA256 hashes.
Client: performs build including generation of hashes
Client (at some point later): ships hashes and operations to server somehow
Server (upon receipt): stores queries+hashes somehow
Client (after server has stored): is deployed
Since the client can't be deployed until the server has stored the queries/hashes, there is indeed coordination. The coordination is incredibly lightweight compared to alternatives where the server must generate hashes during the client build process.
Thanks for your feedback everyone! I've adopted some of the feedback and replied to others. Keep it coming! |
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.
lgtm, this is amazing
I've also written a piece on "trusted documents" - essentially the type of persisted documents that you can build an allowlist with (also known as "persisted queries" and various other names): https://benjie.dev/graphql/trusted-documents My hope is that "trusted documents" can become the preferred term when persisted documents are used as an allowlist (i.e. where your developers have written them), since it cannot be confused with other techniques such as "automatic persisted queries" (APQ), since these techniques involve no trust. |
Regarding If |
@martinbonnin I see what you mean, but |
@benjie right. Maybe "Appendix B: Reserved keys" 🙃? I don't really know... |
In addition to the `doc_id` field documented in the relay docs and the apollo client extension format. The draft spec appendix (graphql/graphql-over-http#264) uses the `documentId` key. part of GB-6253 (the second part is docs)
…#1557) In addition to the `doc_id` field documented in the relay docs and the apollo client extension format. The draft spec appendix (graphql/graphql-over-http#264) uses the `documentId` key. part of GB-6253 (the second part is docs)
identified document within a _persisted document request_ and know that it is | ||
trusted. | ||
|
||
Note: When used solely as a bandwidth optimization, an error-based mechanism |
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.
As we hint here at APQ should we have an opinion on the runtime vs build time generation of documents where we explicitly state that build-time gets you all the security benefits while runtime does not?
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'm happy with the wording as-is to cover this. Note that Persisted Documents are not necessarily trusted documents. All trusted documents are persisted documents, but not all persisted documents are trusted documents.
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.
Should we mention "auto persisted queries" explicitely?
Note: When used solely as a bandwidth optimization, sometimes referred as "auto persisted queries", ...
Note: When persisting a document it is generally good practice for the client to | ||
issue both the GraphQL Document and the document identifier to the server; the | ||
server would then regenerate the document identifier from the GraphQL Document | ||
independently, and check that the identifiers match before storing the Document. | ||
An alternative but equally valid approach has the client issue the GraphQL | ||
Document to the server, and the server returns an arbitrary _custom document | ||
identifier_ that the client would incorporate into its bundle. |
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.
We should write here that the operation identifier to persisted document mapping is usually a build artifact shared by the graphql client and graphql server.
The server can use an external "store" as the source for resolving a document identifier to an actual document
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.
Would you like to propose edits in the form of a PR? Perhaps you're just suggesting an extension to this paragraph, such as:
identifier_ that the client would incorporate into its bundle. Either way, the operation identifier and persisted document mappings are usually a build artefact shared by the client and server.
Note: The server will typically retrieve persisted documents from a "store" at run-time, using the identifier as the lookup. The store could be files on the filesystem, a database, a durable in-memory key-value store, or anywhere else suitable for retrieving a value by a key.
identified document within a _persisted document request_ and know that it is | ||
trusted. | ||
|
||
Note: When used solely as a bandwidth optimization, an error-based mechanism |
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.
Should we mention "auto persisted queries" explicitely?
Note: When used solely as a bandwidth optimization, sometimes referred as "auto persisted queries", ...
|
||
The server should retrieve the GraphQL Document identified by the {documentId} | ||
parameter. If the server fails to retrieve the document, it MUST respond with a | ||
well-formed _GraphQL response_ consisting of a single error. Otherwise, it |
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.
+1 to non-normative example. Attempt:
Note: typically, the error allows recognizing failures to retrieve a document:
\```json
{
"errors": [{
"message: "unknown document identifier"
}]
}
\```
Implementation may add a dedicated error code in the response extensions as described in [the error result format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
identification methods by ensuring that the prefix starts `x-`; otherwise, all | ||
prefixes are reserved for reasons of future expansion. | ||
|
||
### SHA256 Hex Document Identifier |
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.
To be pedantic there, I'd argue that sharing the "sha256" method is still coordination between the client and server. Plus the documents need to be actually transferred (on a pen drive or avian carrier!).
So coordination is always required but sha256 is a convenient and widespread default which we recommend (hence the should)?
A _document identifier_ must either be a _prefixed document identifier_ or a | ||
_custom document identifier_. |
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.
Should we give a formal BNF syntax? Maybe restrict the identifiers to alpha numeric? GraphQL names maybe?
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.
Would you like to submit a PR to my PR to add this?
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.
Sounds like a plan. This week's quite busy but I'll aim for next week! (famous last words 😅 )
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.
That was the longest week ever but attempt at formal syntax is here
Co-authored-by: Martin Bonnin <[email protected]>
Note: A common alternative pattern is to use a dedicated URL for each _persisted | ||
operation_ (e.g. | ||
`https://example.com/graphql/sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b`). | ||
|
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.
Can we add a section on i.e. returning 404 for persisted-documents we can't find and maybe even 400 if they don't leverage an allowed prefix?
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 think for the URL format 404 should be encouraged. I've been writing up a change to the appendix which encourages the URL format, but haven't had time to finish it yet; I've just raised a PR for my WIP so we have something to easily reference: #305
IMO for the non-URL version (traditional), 404 should not be used - it suggests that the /graphql
endpoint is not found, which would be confusing.
This RFC for Persisted Documents as currently written is now supported in GraphQL.NET 8.0.1 and GraphQL.NET Server 8.0.1. Please try it out! Notes:
Non-breaking changes to this RFC will be implemented within GraphQL.NET as needed/requested on an ongoing basis. Breaking changes within GraphQL.NET occur only upon major version releases. |
Addresses #38 (though not the "automatic" version; though that can be built on top of this spec).
This is a first draft.