Beim Erstellen moderner Microservices ist die Wahrscheinlichkeit hoch, dass du bereits mit REST-APIs gearbeitet hast. Vielleicht bist du auch mit Spring Boot und Spring Web vertraut und kennst die Möglichkeit, auf Header mithilfe von Annotationen wie @RequestHeader
zuzugreifen. Es ist einfach, vertraut und flexibel.
Als ich kürzlich mit GraphQL arbeitete, stieß ich jedoch auf eine Herausforderung: Wie greife ich in Spring-for-GraphQL auf Request-Header zu? Genauer genommen musste ich mit benutzerdefinierten Headern arbeiten, um spezielle Geschäftslogiken auszuführen, und es war nicht sofort ersichtlich, wie das funktionieren sollte. Nach einer längeren Suche fand ich die Lösung. Ein kleiner Spoiler: Es erfordert das Erstellen eines benutzerdefinierten Request-Interceptors. Der vollständige Code für diese Lösung ist auf GitHub zu finden.
In diesem Beitrag führe ich dich durch das Problem, auf das ich gestoßen bin, erkläre meinen Lösungsweg und zeige dir, wie ich letztendlich eine flexible Möglichkeit gefunden habe, HTTP-Header in einer GraphQL-Umgebung zu verwalten. Lass uns direkt loslegen!
In einer typischen REST-API, die mit Spring Boot erstellt wurde, ist der Zugriff auf HTTP-Header ziemlich einfach. Man verwendet einfach @RequestHeader
, um auf die benötigten Header zuzugreifen:
@RestController @RequestMapping("/api/tasks") class AddTaskRestController(private val useCase: AddTaskUseCase) { private val log = KotlinLogging.logger {} @PostMapping fun addTask( @RequestBody input: TaskInput, @RequestHeader(X_USER_ID) userId: String ): TaskDto { log.debug { "Received REST-request to add task: $input" } val command = input.toAddTaskCommand(userId) val task = useCase.addTask(command) return TaskDto.from(task) } }
Als ich kürzlich jedoch mit Spring for GraphQL arbeitete, stellte ich fest, dass die Dinge dort nicht ganz so einfach sind. GraphQL funktioniert etwas anders als REST, und die Spring-for-GraphQL-Bibliothek bietet keine direkte Möglichkeit, Header aus einer Anfrage zu extrahieren. Dennoch blieb meine Herausforderung bestehen: Ich musste auf Header wie X-User-ID
zugreifen, um sie in unserer Anwendung zu verwenden. Es war eine kleine, aber wichtige Herausforderung.
Bevor ich die Lösung erläutere, möchte ich ein bisschen meinen Gedankengang teilen. Als ich merkte, dass Spring for GraphQL keine direkte Möglichkeit bot, um auf HTTP-Header zuzugreifen, hielt ich inne und dachte nach: Ist es immer richtig, solche Probleme selbst zu lösen? Manchmal existieren die Einschränkungen eines Frameworks aus gutem Grund – vielleicht ist es eine bewusste Designentscheidung, oder es gibt eine bessere Praxis, die ich übersehen habe.
Also begann ich zu forschen. Warum sollte Spring for GraphQL diese Funktion nicht zur Verfügung steleln? Ich fand heraus, dass das Framework für die Bearbeitung von Anfragen mehrere Protokolle unterstützt – wie HTTP, WebSocket und RSocket. Diese Protokolle unterscheiden sich erheblich. Einige behandeln Header unterschiedlich, andere haben gar keine Header. Daher schloss ich, dass es eine Designentscheidung sein könnte, keine eingebaute Möglichkeit zum Lesen von Headern zu bieten, um das Framework so generisch wie möglich zu halten.
Mit dieser Erkenntnis entschied ich mich, das Problem selbst zu lösen. Die Dokumentation schlug sogar ein Muster zur Handhabung eines solchen Anwendungsfalls vor, was mir das Vertrauen gab, weiterzumachen.
Das vorgeschlagene Muster umfasste einen kombinierten Ansatz:
die Implementierung eines Request-Interceptors und die Nutzung des GraphQL-Kontextmechanismus.
Der Kontextmechanismus dient als gemeinsamer Datenspeicher,
der während des gesamten Lebenszyklus einer GraphQL-Anfrage verfügbar ist.
Er kann alle Arten von anfragespezifischen Daten enthalten.
Einmal im Kontext, können Werte einfach in den Resolver-Methoden mit der Annotation @ContextValue
abgerufen werden.
Durch die Erstellung eines benutzerdefinierten WebGraphQlInterceptor
konnte ich die Header aus der GraphQL-Anfrage extrahieren und dem Kontext hinzufügen.
Der Interceptor sieht dabei wie folgt aus:
@Component class GraphQlRequestHeaderInterceptor( private val log: KLogger = KotlinLogging.logger {} ) : WebGraphQlInterceptor { override fun intercept(request: WebGraphQlRequest, chain: Chain): Mono<WebGraphQlResponse> { val headers = getHeadersFromRequest(request) log.trace { "Found ${headers.size} headers that will be added to the GQL-context: $headers" } addHeadersToGraphQLContext(request, headers) return chain.next(request) } private fun getHeadersFromRequest(request: WebGraphQlRequest): Map<String, Any> { return request.headers.mapValues { it.value.first() } } private fun addHeadersToGraphQLContext( request: WebGraphQlRequest, customHeaders: Map<String, Any> ) = request.configureExecutionInput { _, builder -> builder.graphQLContext(customHeaders).build() } }
WebGraphQlInterceptor
und dessen Methode intercept()
. Diese wird für jede GraphQL-Anfrage aufgerufen.getHeadersFromRequest()
lädt alle Header aus der Anfrage, ohne eine Filterung anzuwenden. Dadurch wird volle Flexibilität beim Zugriff auf alle erforderlichen Header-Werte gewährleistet.addHeadersToGraphQLContext()
fügt die Header dem Kontext hinzu, sodass sie in allen Resolvern zugänglich sind.Sobald die Header im GraphQL-Kontext sind, kannst du einfach auf sie zugreifen. Du musst sie lediglich über @ContextValue
abrufen. Hier ist ein Beispiel für einen Resolver, der diese Annotation verwendet:
@Controller class AddTaskController(private val useCase: AddTaskUseCase) { private val log = KotlinLogging.logger {} @MutationMapping fun addTask( @Argument payload: TaskInput, @ContextValue(X_USER_ID) userId: String ): TaskDto { log.debug { "Received graphql-request to add task: $payload" } val command = payload.toAddTaskCommand(userId) val task = useCase.addTask(command) return TaskDto.from(task) } }
Um diese Lösung weiter zu verbessern, wollte ich untersuchen, wie wir den Zugriff auf Header noch intuitiver gestalten könnten.
Während @ContextValue
gut funktioniert, kann es für diejenigen, die etwas wie @RequestHeader
erwarten,ungewohnt sein.
Daher tauchte ich tiefer in das Framework ein und fand schließlich eine Lösung:
den HandlerMethodArgumentResolver
.
Mit einem solchen kann man definieren, wie Parameter, die mit einer bestimmten Annotation markiert sind, aufgelöst werden sollen.
Basierend auf diesem Ansatz entwickelte ich eine prototypische Lösung mit einem Resolver für Argumente, die mit @RequestHeader
anotiert wurden. Obwohl diese funktionierte, wurde mir schnell klar, dass dieser Ansatz lediglich die Funktionalität von @ContextValue
duplizierte und zusätzliche Komplexität einführte. Trotz des Vorteils einer höheren Vertrautheit rate ich daher von dieser Option ab. Die Verwendung von @ContextValue
ist meiner Meinung nach der bessere Weg, da sie die schlankere und elegantere Lösung darstellt.
class RequestHeaderArgumentResolver : HandlerMethodArgumentResolver { override fun supportsParameter( parameter: MethodParameter ) = parameter.hasParameterAnnotation(RequestHeader::class.java) override fun resolveArgument( parameter: MethodParameter, environment: DataFetchingEnvironment ): Any? { val headerName = getKeyOfRequiredHeader(parameter) return environment.graphQlContext.get(headerName) } private fun getKeyOfRequiredHeader( parameter: MethodParameter ): String { val headerAnnotation = parameter.getParameterAnnotation(RequestHeader::class.java) return headerAnnotation?.name ?: throw IllegalArgumentException( "No header name specified for @RequestHeader annotation" ) } }
Durch die Verwendung eines benutzerdefinierten WebGraphQlInterceptor
schließen wir die Lücke zwischen HTTP-Headern und dem GraphQL-Kontext. Alles in allem bietet die Lösung mehrere Vorteile:
@ContextValue
bietet eine ähnliche & daher vertraute Lösung für Entwickler, die an Annotationen wie @RequestHeader
gewohnt sind.Der Zugriff auf HTTP-Header in Spring for GraphQL ist nicht so einfach wie bei REST, aber durch die Erstellung eines benutzerdefinierten Interceptors und die Nutzung des GraphQL-Contexts können wir ähnliche Ergebnisse erzielen. Insgesamt bietet der GraphQlRequestHeaderInterceptor
eine elegante Lösung, um Header in deinen Resolvern verfügbar zu machen.
Rückblickend habe ich erkannt, dass es wichtig ist, die Einschränkungen eines Frameworks zu verstehen und zu hinterfragen, bevor man in individuelle Lösungen eintaucht. Manchmal übersehen wir eine einfachere oder bessere Herangehensweise. Ich glaube, dass das Überdenken unserer Strategien zu effektiveren Lösungen führen kann und die Erfolgsaussichten erhöht.
Vielen Dank fürs Lesen! 😊
Den gesamten Code für diese Lösung findest du auf GitHub, wenn du ihn dir genauer ansehen möchtest.
This post was originally published in English on Medium.com.
Rechtliches