Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Conversation

benjie
Copy link
Member

@benjie benjie commented Oct 9, 2023

Addresses #38 (though not the "automatic" version; though that can be built on top of this spec).

This is a first draft.

Copy link
Member

@JoviDeCroock JoviDeCroock left a 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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member Author

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 🤨

Copy link
Member

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

spec/Appendix A -- Persisted Documents.md Show resolved Hide resolved
@benjie
Copy link
Member Author

benjie commented Oct 9, 2023

I think the RFC document still warrants discussion; let's leave it until after the meeting.

@benjie
Copy link
Member Author

benjie commented Oct 9, 2023

Added to October 26th agenda: https://github.com/graphql/graphql-over-http/blob/main/working-group/agendas/2023/2023-10-26.md

spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved

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
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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)

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.
Copy link
Contributor

@Shane32 Shane32 Oct 10, 2023

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.

Copy link
Member Author

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.

Copy link
Contributor

@n1ru4l n1ru4l May 21, 2024

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

Copy link
Member Author

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At Airbnb we append the operation name to the URL for the GraphQL requests for easier debugging and tracing. I wonder if this spec can include an example or implementation of such solutions:

Screenshot 2023-10-10 at 11 10 30 AM

Copy link
Member Author

@benjie benjie Oct 10, 2023

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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Contributor

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:

  1. Should the query storage mechanism change, the identifiers will remain constant (but the same would be true of a GUID)
  2. Inherit deduplication for stored queries
  3. 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thank you @Shane32 @benjie .
My point is not about the prefix or the method but for the need for encryption? a user can decide to use operation-1 as the key (instead of an actual computed hash), and the result will be the same 🤔 🤔 🤔

Copy link
Member Author

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.

Copy link
Member Author

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!).

Copy link
Member

@dotansimha dotansimha Oct 12, 2023

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?

Copy link
Member Author

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.

Copy link
Contributor

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)?

Copy link
Member Author

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.

@benjie
Copy link
Member Author

benjie commented Oct 10, 2023

Thanks for your feedback everyone! I've adopted some of the feedback and replied to others. Keep it coming!

Copy link

@mcollina mcollina left a 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

@benjie
Copy link
Member Author

benjie commented Oct 12, 2023

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.

@martinbonnin
Copy link
Contributor

Regarding documentId, would it make sense to add it to the main spec spec page as well? Somewhere around here, in the "Request parameters" section?

If documentId is a reserved key, might as well allocated it in the main spec page? Going to an appendix for that feels a bit off.

@benjie
Copy link
Member Author

benjie commented Oct 13, 2023

@martinbonnin I see what you mean, but documentId is meaningless to a GraphQL-over-HTTP request; it's only relevant to a persisted document request. I guess we could add it as a "reserved key" ("a GraphQL-over-HTTP request must never contain a property documentId"); but at the moment all unspecified keys are effectively reserved (if you need to add a key, it should go in extensions) so that seems redundant.

@martinbonnin
Copy link
Contributor

martinbonnin commented Oct 13, 2023

@benjie right. Maybe "Appendix B: Reserved keys" 🙃? I don't really know...
It'd be interesting to compare to what other protocols with optional features have done in the past. I don't have an example at hand right now but I'll keep looking.

tomhoule added a commit to grafbase/grafbase that referenced this pull request Apr 4, 2024
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)
tomhoule added a commit to grafbase/grafbase that referenced this pull request Apr 4, 2024
…#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
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Contributor

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", ...

Comment on lines 121 to 127
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.
Copy link
Contributor

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

Copy link
Member Author

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.

spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
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
Copy link
Contributor

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", ...

spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved

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
Copy link
Contributor

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)

spec/Appendix A -- Persisted Documents.md Outdated Show resolved Hide resolved
identification methods by ensuring that the prefix starts `x-`; otherwise, all
prefixes are reserved for reasons of future expansion.

### SHA256 Hex Document Identifier
Copy link
Contributor

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)?

Comment on lines +32 to +33
A _document identifier_ must either be a _prefixed document identifier_ or a
_custom document identifier_.
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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 😅 )

Copy link
Contributor

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

Note: A common alternative pattern is to use a dedicated URL for each _persisted
operation_ (e.g.
`https://example.com/graphql/sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b`).

Copy link
Member

@JoviDeCroock JoviDeCroock Jul 26, 2024

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?

Copy link
Member Author

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.

@Shane32
Copy link
Contributor

Shane32 commented Aug 23, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants