Wie man ETag-Header in einer GraphQL-API mit NestJS implementiert
Was ist ein ETag?
Das ETag oder Entitäts-Tag ist ein Bestandteil des HTTP-Antwort-Headers, der als Kennung für eine spezifische Ressourcenversion dient. Dieser Mechanismus verbessert die Effizienz des Caches und spart Bandbreite, da ein Webserver oder Backend-Dienst keine vollständige Antwort erneut senden muss, wenn sich der Inhalt nicht geändert hat.
Der ETag-Wert wird basierend auf der angeforderten Ressource berechnet. Die Methode zur Generierung von ETag-Werten ist nicht festgelegt. Typischerweise ist der ETag-Wert ein Hash des Inhalts, ein Hash des letzten Änderungszeitstempels oder nur eine Revisionsnummer. Für die aktuelle Implementierung verwenden wir das npm-Paket etag
, um den ETag-Wert basierend auf dem Antwortinhalt zu generieren.
Wie funktioniert es?
Wenn ein Client eine Ressource anfordert, wird ein eindeutiges ETag basierend auf dem Inhalt der Ressource berechnet und dem ETag-Antwortheader mit dem Wert des aktuellen Datums und des letzten Änderungs-Headers hinzugefügt.
Bei nachfolgenden Anfragen des Clients mit einem zuvor generierten ETag vergleicht der Server den ETag des Clients (gesendet mit If-None-Match
) mit dem ETag für die aktuelle Version der Ressource. Wenn beide Werte übereinstimmen (was darauf hinweist, dass keine Änderungen an der Ressource vorgenommen wurden), antwortet der Server mit einem Status 304 Not Modified ohne Body und informiert den Client, dass die zwischengespeicherte Antwort gültig (aktuell) bleibt.
Implementierung von ETag-Headern in einer NestJS GraphQL-API
Die Implementierung von ETag in einer GraphQL-API erfordert möglicherweise zusätzlichen Aufwand aufgrund von GraphQL-Filtern, die direkt vor der Übertragung der Antwort über das Internet ausgeführt werden. Diese Filter formatieren den Antwortinhalt innerhalb eines Feldes namens "Daten", melden Fehler im Feld "Fehler", validieren die Antwort gegen die angeforderte Abfrage und setzen den Antwortcode auf 200.
Einrichtung
Die einzige erforderliche Konfiguration besteht darin, das Standardverhalten einer Express-Anwendung mit dem ETag-Header zu deaktivieren. Standardmäßig generiert Express (verwendet von NestJS) ein ETag für alle GET-HTTP-Anfragen. Dies kann wie folgt deaktiviert werden:
app.getHttpAdapter().getInstance().set('etag', false);
Apollo Server Plugin
Plugins sind TypeScript (JavaScript)-Objekte, die eine oder mehrere Funktionen implementieren, um auf Ereignisse zu reagieren. Wir werden den in der NestJS-Dokumentation beschriebenen Prozess für ein benutzerdefiniertes Plugin verwenden (
import { Plugin } from '@nestjs/apollo';
import {
ApolloServerPlugin,
GraphQLRequestContextWillSendResponse,
GraphQLRequestListener,
} from '@apollo/server';
@Plugin()
export class EtagPlugin implements ApolloServerPlugin {
async requestDidStart(): Promise<GraphQLRequestListener<ApolloServerPlugin>> {
return {
willSendResponse: async (
requestContext: GraphQLRequestContextWillSendResponse<ApolloServerPlugin>,
) => {
const req = requestContext.request;
const res = requestContext.response;
//..
},
};
}
}
Die Methode requestDidStart
wird einmal pro Anfrage vor dem Parsen oder Validieren der Anfrage aufgerufen. Dieser Hook kann ein Objekt mit feiner abgestuften Hooks zurückgeben. Zum Beispiel wird der Hook willSendResponse
aufgerufen, wenn die ursprüngliche Antwort (einschließlich aufgelöster Felder) gesendet werden soll.
Nun haben wir alle erforderlichen Komponenten, um den ETag-Header in einer GraphQL-API zu implementieren. Die Schritte sind wie folgt:
ETags Generieren:
Wenn ein Client eine Ressource anfordert, wird ein eindeutiges ETag basierend auf dem Inhalt der Ressource berechnet und in der GraphQL-Antwort bereitgestellt.
@Plugin()
export class EtagPluging implements ApolloServerPlugin {
async requestDidStart(): Promise<GraphQLRequestListener<ApolloServerPlugin>> {
return {
willSendResponse: async (
requestContext: GraphQLRequestContextWillSendResponse<ApolloServerPlugin>,
) => {
const req = requestContext.request;
const res = requestContext.response;
const responseEtag = etag(JSON.stringify(res)); //<-- etag npm package
res.http.headers.set('etag', responseEtag);
//....
},
};
}
}
Behandlung bedingter Anfragen
@Plugin()
export class EtagPluging implements ApolloServerPlugin {
async requestDidStart(): Promise<GraphQLRequestListener<ApolloServerPlugin>> {
return {
willSendResponse: async (
requestContext: GraphQLRequestContextWillSendResponse<ApolloServerPlugin>,
) => {
const req = requestContext.request;
const res = requestContext.response;
const responseEtag = etag(JSON.stringify(res)); //<-- etag npm package
res.http.headers.set('etag', responseEtag);
const clientDate = req.http.headers.get('if-modified-since');
const clientEtag = req.http.headers.get('if-none-match');
const lastModified = this.getLastModified(responseEtag);
const currentDate = new Date().toUTCString();
if (lastModified) {
responseHeaders.set('last-modified', lastModified);
if (responseEtag === clientEtag && clientDate === lastModified) {
res.http.status = 304;
res.body = null;
}
} else {
this.setLastModified(responseEtag, currentDate);
responseHeaders.set('last-modified', currentDate);
}
},
};
}
}
Wenn ein Client nachfolgende Anfragen für dieselbe Ressource stellt, sollte er das erhaltene ETag im If-None-Match
Header und den erhaltenen letzten Änderungszeitstempel im If-Modified-Since
Header enthalten.
Wenn eine Anfrage auf dem Server mit einem If-None-Match
Header empfangen wird, sollte das bereitgestellte ETag mit dem aktuellen ETag der Ressource verglichen werden. Ähnlich sollte, wenn der If-Modified-Since
Header empfangen wird, der bereitgestellte Zeitstempel mit dem aktuellen letzten Änderungszeitstempel der Ressource verglichen werden. Wenn eine Übereinstimmung besteht, sollte der Server mit einem Status 304 Not Modified antworten.
Zuletzt geändertes Update bei Ressourcenänderung:
Um sicherzustellen, dass Clients die neueste Version einer Ressource erhalten sollten Sie den Zeitstempel der letzten Änderung aktualisieren, wenn Änderungen vorgenommen werden. Die Methode zur Speicherung des zuletzt geänderten Werts ist nicht festgelegt und kann auf verschiedene Weise implementiert werden, beispielsweise durch Zwischenspeicherung oder in einer Datenbank.