From 25c6968754ae7ad39983095ffafe73a1c745808c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 9 Oct 2023 13:25:38 +0100 Subject: [PATCH 1/7] Compress URL further --- spec/GraphQLOverHTTP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/GraphQLOverHTTP.md b/spec/GraphQLOverHTTP.md index 434711b5..38022180 100644 --- a/spec/GraphQLOverHTTP.md +++ b/spec/GraphQLOverHTTP.md @@ -309,7 +309,7 @@ either omit {operationName} or set it to the empty string. If we wanted to execute the following GraphQL query: ```raw graphql example -query($id: ID!){user(id:$id){name}} +query($id:ID!){user(id:$id){name}} ``` With the following query variables: @@ -321,7 +321,7 @@ With the following query variables: This request could be sent via an HTTP GET as follows: ```url example -http://example.com/graphql?query=query(%24id%3A%20ID!)%7Buser(id%3A%24id)%7Bname%7D%7D&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D +http://example.com/graphql?query=query(%24id%3AID!)%7Buser(id%3A%24id)%7Bname%7D%7D&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D ``` GET requests MUST NOT be used for executing mutation operations. If the values From 786394125ea202a85741ad26395fefcfc02bcd18 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 9 Oct 2023 13:26:05 +0100 Subject: [PATCH 2/7] Add Appendix A: Persisted Documents --- spec/Appendix A -- Persisted Documents.md | 256 ++++++++++++++++++++++ spec/GraphQLOverHTTP.md | 2 + 2 files changed, 258 insertions(+) create mode 100644 spec/Appendix A -- Persisted Documents.md diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md new file mode 100644 index 00000000..06ac8cbf --- /dev/null +++ b/spec/Appendix A -- Persisted Documents.md @@ -0,0 +1,256 @@ +# A. Appendix: Persisted Documents + +This appendix defines an optional extension to the GraphQL-over-HTTP protocol +that allows for the usage of "persisted documents". + +:: A _persisted document_ is a GraphQL document (strictly: an +[`ExecutableDocument`](https://spec.graphql.org/draft/#ExecutableDocument)) that +has been persisted such that the server may retrieve it based on an identifier +indicated in the HTTP request. + +This feature can be used as an operation allow-list, as a way of improving the +caching of GraphQL operations, or just as a way of reducing the bandwidth +consumed from sending the full GraphQL Document to the server on each request. + +Typically, support for the _persisted document_ feature is implemented via a +"middleware" that sits in front of the GraphQL service and transforms a +_persisted document request_ into a _GraphQL-over-HTTP request_. + +:: A _persisted operation_ is a _persisted document_ which contains only one +GraphQL operation and all the fragments this operation references (recursively). + +## Identifying a Document + +:: A _document identifier_ is a string-based identifier that uniquely identifies +a GraphQL Document. + +Note: A _document identifier_ must be unique, otherwise there is a risk of +responses confusing the client. Even if the selection sets are identical, even +whitespace changes may change the location from which errors are raised, and +thus should generate different document identifiers. + +A _document identifier_ must either be a _prefixed document identifier_ or a +_custom document identifier_. + +### Prefixed Document Identifier + +:: A _prefixed document identifier_ is a document identifier that contains at +least one colon symbol (`:`). The text before the first colon symbol is called +the {prefix}, and the text after it is called the {payload}. The {prefix} +identifies the method of identification used. Applications may use their own +identification methods by ensuring that the prefix starts `x-`; otherwise, all +prefixes are reserved for reasons of future expansion. + +### SHA256 Hex Document Identifier + +:: A _SHA256 hex document identifier_ is a _prefixed document identifier_ where +{prefix} is `sha256` and {payload} is 64 hexadecimal characters (in lower case). + +The payload of a _SHA256 hex document identifier_ must be produced via the +lower-case hexadecimal encoding of the SHA256 hash (as specified in +[RFC4634](https://datatracker.ietf.org/doc/html/rfc4634)) of the Source Text of +the GraphQL Document (as specified in +[the Language section of the GraphQL specification](https://spec.graphql.org/draft/#sec-Language)). + +A service which accepts a _persisted document request_ SHOULD support the +_SHA256 hex document identifier_ for compatibility. + +#### Example + +The following GraphQL query (with no trailing newline): + +```graphql example +query ($id: ID!) { + user(id: $id) { + name + } +} +``` + +Would have the following _SHA256 hex document identifier_: + +```example +sha256:7dba4bd717b41f10434822356a93c32b1fb4907b983e854300ad839f84cdcd6e +``` + +Whereas the same query with all optional whitespace omitted: + +```raw graphql example +query($id:ID!){user(id:$id){name}} +``` + +Would have this different _SHA256 hex document identifier_: + +```example +sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b +``` + +### Custom Document Identifier + +:: A _custom document identifier_ is a document identifier that contains no +colon symbols (`:`). The meaning of a custom document identifier is +implementation specific. + +Note: A 32 character hexadecimal _custom document identifier_ is likely to be an +MD5 hash of the GraphQL document, as traditionally used by Relay. + +## Persisting a Document + +A client utilizing persisted documents MUST generate a _document identifier_ for +each GraphQL Document it wishes to issue to the server, and SHOULD ensure that +the server can retrieve this GraphQL Document given the document identifier. + +Note: The method through which the client and server achieve this is +implementation specific. Typically persisted documents are stored into some kind +of trusted shared key-value store at client build time (either directly, or +indirectly via an authenticated request to the server) such that the server may +retrieve them given the identifier at request time. It is generally good +practice for the client to issue both the GraphQL Document and the document +identifier to the server; the server should then regenerate the document +identifier from the GraphQL Document independently, and check that the +identifiers match before storing the Document. An alternative approach has the +client issue the GraphQL Document to the server, and the server returns an +arbitrary _custom document identifier_ that the client should incorporate into +its bundle. + +When using persisted documents as an operation allowlist, the client MUST +persist the documents it uses ahead of time in a secure manner (preventing +untrusted third parties from adding their own persisted document) such that the +server will be able to retrieve the identified document within a _persisted +document request_ and know that it is trusted. + +## Persisted Document Request + +A server MAY accept a _persisted document request_ via `GET` or `POST`. + +### Persisted Document Request Parameters + +:: 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. +- {operationName} - (_Optional_, string): The name of the Operation in the + identified Document to execute. +- {variables} - (_Optional_, map): Values for any Variables defined by the + Operation. +- {extensions} - (_Optional_, map): This entry is reserved for implementors to + extend the protocol however they see fit. + +### GET + +For a _persisted document request_ using HTTP GET, parameters SHOULD be provided +in the query component of the request URL, encoded in the +`application/x-www-form-urlencoded` format as specified by the +[WhatWG URLSearchParams class](https://url.spec.whatwg.org/#interface-urlsearchparams). + +The {documentId} parameter must be a string _document identifier_. + +The {operationName} parameter, if present, must be a string. + +Each of the {variables} and {extensions} parameters, if used, MUST be encoded as +a JSON string. + +Setting the value of the {operationName} parameter to the empty string is +equivalent to omitting the {operationName} parameter. + +A client MAY provide the _persisted document request_ parameters in another way +if the server supports that. + +Note: A common alternative pattern is to use a dedicated URL for each _persisted +operation_ (e.g. +`https://example.com/graphql/sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b`). + +#### Canonical Parameters + +Parameters SHOULD be provided in the order given in the list above, any optional +parameters which have no value SHOULD be omitted, and parameters encoded as JSON +string SHOULD use the most compressed form (with all optional whitespace +omitted). A server MAY reject requests where this is not adhered to. + +Note: Ensuring that parameters are in their canonical form helps improve cache +hit ratios. + +#### Example + +Executing the GraphQL Document identified by +`"sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b"` with +the following query variables: + +```raw json example +{"id":"QVBJcy5ndXJ1"} +``` + +This request could be sent via an HTTP GET as follows: + +```url example +https://example.com/graphql?documentId=sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D +``` + +### POST + +For a _persisted document request_ using HTTP POST, the request MUST have a body +which contains values of the _persisted document request_ parameters encoded in +one of the officially recognized GraphQL media types, or another media type +supported by the server. + +#### JSON Encoding + +When encoded in JSON, a _persisted document request_ is encoded as a JSON object +(map), with the properties specified by the persisted document request: + +- {documentId} - the string identifier for the Document +- {operationName} - an optional string +- {variables} - an optional object (map), the keys of which are the variable + names and the values of which are the variable values +- {extensions} - an optional object (map) + +#### Example + +If we wanted to execute the following GraphQL query: + +```raw graphql example +query ($id: ID!) { + user(id: $id) { + name + } +} +``` + +With the following query variables: + +```json example +{ + "id": "QVBJcy5ndXJ1" +} +``` + +This request could be sent via an HTTP POST to the relevant URL using the JSON +encoding with the headers: + +```headers example +Content-Type: application/json +Accept: application/graphql-response+json +``` + +And the body: + +```json example +{ + "documentId": "sha256:7dba4bd717b41f10434822356a93c32b1fb4907b983e854300ad839f84cdcd6e", + "variables": { + "id": "QVBJcy5ndXJ1" + } +} +``` + +## Persisted Document Response + +When a server that implements _persisted documents_ receives a well-formed +_persisted document request_, it must return a well‐formed _GraphQL response_. + +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 +should construct a _GraphQL-over-HTTP request_ using this document and the other +parameters of the _persisted document request_, and then follow the details in +the [Response section](#sec-Response). diff --git a/spec/GraphQLOverHTTP.md b/spec/GraphQLOverHTTP.md index 38022180..ebf3e436 100644 --- a/spec/GraphQLOverHTTP.md +++ b/spec/GraphQLOverHTTP.md @@ -738,3 +738,5 @@ payload is `application/json` then the client MUST NOT rely on the body to be a well-formed _GraphQL response_ since the source of the response may not be the server but instead some intermediary such as API gateways, proxies, firewalls, etc. + +# [Appendix: Persisted Document](Appendix%20A%20--%20Persisted%20Documents.md) From a1a2c257c093a5f843dbf045b9c1ade4a56f6d7f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Mon, 9 Oct 2023 13:48:38 +0100 Subject: [PATCH 3/7] GET must not be used for mutations --- spec/Appendix A -- Persisted Documents.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md index 06ac8cbf..f7ba6a7a 100644 --- a/spec/Appendix A -- Persisted Documents.md +++ b/spec/Appendix A -- Persisted Documents.md @@ -160,6 +160,12 @@ Note: A common alternative pattern is to use a dedicated URL for each _persisted operation_ (e.g. `https://example.com/graphql/sha256:71f7dc5758652baac68e4a10c50be732b741c892ade2883a99358f52b555286b`). +GET requests MUST NOT be used for executing mutation operations. If a mutation +operation is indicated by the value of {operationName} and the GraphQL Document +identified by {documentId}, the server MUST respond with error status code `405` +(Method Not Allowed) and halt execution. This restriction is necessary to +conform with the long-established semantics of safe methods within HTTP. + #### Canonical Parameters Parameters SHOULD be provided in the order given in the list above, any optional From 6e9cb85c99402e8c3e37822aee3ad8bb02c71bff Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 10 Oct 2023 14:17:02 +0100 Subject: [PATCH 4/7] Specify character encoding. --- spec/Appendix A -- Persisted Documents.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md index f7ba6a7a..1d673d0b 100644 --- a/spec/Appendix A -- Persisted Documents.md +++ b/spec/Appendix A -- Persisted Documents.md @@ -50,7 +50,8 @@ The payload of a _SHA256 hex document identifier_ must be produced via the lower-case hexadecimal encoding of the SHA256 hash (as specified in [RFC4634](https://datatracker.ietf.org/doc/html/rfc4634)) of the Source Text of the GraphQL Document (as specified in -[the Language section of the GraphQL specification](https://spec.graphql.org/draft/#sec-Language)). +[the Language section of the GraphQL specification](https://spec.graphql.org/draft/#sec-Language)) +encoded using the UTF-8 character set. A service which accepts a _persisted document request_ SHOULD support the _SHA256 hex document identifier_ for compatibility. From 433b37f20b7f218f4470e526d3ab9be0fbcd39d1 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 10 Oct 2023 14:30:32 +0100 Subject: [PATCH 5/7] Rewrite 'Persisting a document' section --- spec/Appendix A -- Persisted Documents.md | 50 +++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md index 1d673d0b..cd470279 100644 --- a/spec/Appendix A -- Persisted Documents.md +++ b/spec/Appendix A -- Persisted Documents.md @@ -97,28 +97,34 @@ MD5 hash of the GraphQL document, as traditionally used by Relay. ## Persisting a Document -A client utilizing persisted documents MUST generate a _document identifier_ for -each GraphQL Document it wishes to issue to the server, and SHOULD ensure that -the server can retrieve this GraphQL Document given the document identifier. - -Note: The method through which the client and server achieve this is -implementation specific. Typically persisted documents are stored into some kind -of trusted shared key-value store at client build time (either directly, or -indirectly via an authenticated request to the server) such that the server may -retrieve them given the identifier at request time. It is generally good -practice for the client to issue both the GraphQL Document and the document -identifier to the server; the server should then regenerate the document -identifier from the GraphQL Document independently, and check that the -identifiers match before storing the Document. An alternative approach has the -client issue the GraphQL Document to the server, and the server returns an -arbitrary _custom document identifier_ that the client should incorporate into -its bundle. - -When using persisted documents as an operation allowlist, the client MUST -persist the documents it uses ahead of time in a secure manner (preventing -untrusted third parties from adding their own persisted document) such that the -server will be able to retrieve the identified document within a _persisted -document request_ and know that it is trusted. +A client that wishes to utilize persisted documents for a request must generate +a _document identifier_ for the associated GraphQL Document and should ensure +the server can retrieve this GraphQL Document from the document identifier. The +method through which the client and server achieve this is implementation +specific. + +Note: When used as an operation allowlist, persisted documents are typically +stored into some kind of trusted shared key-value store at client build time +(either directly, or indirectly via an authenticated request to the server) such +that the server may retrieve them given the identifier at request time. This +must be done in a secure manner (preventing untrusted third parties from adding +their own persisted document) such that the server will be able to retrieve the +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 +might be used wherein the client assumes that the document has already been +persisted, but if the request fails due to unknown _document identifier_ the +client issues a follow-up request containing the full GraphQL Document to be +persisted. + +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. ## Persisted Document Request From 6fbc6ed314432ff5cc1efb42a3aced5c3461a2aa Mon Sep 17 00:00:00 2001 From: Benjie Date: Tue, 4 Jun 2024 13:21:25 +0100 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Martin Bonnin --- spec/Appendix A -- Persisted Documents.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md index cd470279..c589e95f 100644 --- a/spec/Appendix A -- Persisted Documents.md +++ b/spec/Appendix A -- Persisted Documents.md @@ -103,8 +103,8 @@ the server can retrieve this GraphQL Document from the document identifier. The method through which the client and server achieve this is implementation specific. -Note: When used as an operation allowlist, persisted documents are typically -stored into some kind of trusted shared key-value store at client build time +Note: When used as an operation allow-list, persisted documents are typically +stored into a trusted shared key-value store at client build time (either directly, or indirectly via an authenticated request to the server) such that the server may retrieve them given the identifier at request time. This must be done in a secure manner (preventing untrusted third parties from adding From 52d56fb36d51c17e08a920510a23bdc2f6a720be Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Jun 2024 13:29:47 +0100 Subject: [PATCH 7/7] Adopt some changes recommended via review --- spec/Appendix A -- Persisted Documents.md | 38 ++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/spec/Appendix A -- Persisted Documents.md b/spec/Appendix A -- Persisted Documents.md index c589e95f..cfda7197 100644 --- a/spec/Appendix A -- Persisted Documents.md +++ b/spec/Appendix A -- Persisted Documents.md @@ -97,34 +97,36 @@ MD5 hash of the GraphQL document, as traditionally used by Relay. ## Persisting a Document -A client that wishes to utilize persisted documents for a request must generate -a _document identifier_ for the associated GraphQL Document and should ensure -the server can retrieve this GraphQL Document from the document identifier. The +To utilize persisted documents for a request, the client must possess a unique +_document identifier_ for the associated GraphQL Document, and the server must +be able to retrieve this GraphQL Document using the document identifier. The method through which the client and server achieve this is implementation specific. Note: When used as an operation allow-list, persisted documents are typically -stored into a trusted shared key-value store at client build time -(either directly, or indirectly via an authenticated request to the server) such -that the server may retrieve them given the identifier at request time. This -must be done in a secure manner (preventing untrusted third parties from adding -their own persisted document) such that the server will be able to retrieve the +stored into a trusted shared key-value store at client build time (either +directly, or indirectly via an authenticated request to the server) such that +the server may retrieve them given the identifier at request time. This must be +done in a secure manner (preventing untrusted third parties from adding their +own persisted document) such that the server will be able to retrieve the 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 +Note: When used solely as a bandwidth optimization, as in the technique known +colloquially as "automatic persisted queries (APQ)," an error-based mechanism might be used wherein the client assumes that the document has already been persisted, but if the request fails due to unknown _document identifier_ the client issues a follow-up request containing the full GraphQL Document to be persisted. -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. +Note: When persisting a document for which the identifier has been derived by +the client, it is generally good practice for the client to issue both the +GraphQL Document and the document identifier to the server; the server could +then regenerate the document identifier from the GraphQL Document independently, +and check that the identifiers match before storing the Document. If the +identifier is not derived on the client then the client must coordinate +retrieval of a document identifier from the server to be incorporated into the +deployed client. ## Persisted Document Request @@ -263,7 +265,7 @@ _persisted document request_, it must return a well‐formed _GraphQL response_. 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 -should construct a _GraphQL-over-HTTP request_ using this document and the other +well-formed _GraphQL response_ consisting of a single error. Otherwise, it will +construct a _GraphQL-over-HTTP request_ using this document and the other parameters of the _persisted document request_, and then follow the details in the [Response section](#sec-Response).