Zurück zur WebseiteInsights

So kannst du in Spring-For-GraphQL auf HTTP-Header zugreifen

By Marco Schäck
Published in Softwareentwicklung
October 08, 2024
4 min read
So kannst du in Spring-For-GraphQL auf HTTP-Header zugreifen

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!

🔍 Das Problem: Zugriff auf Header in GraphQL

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.

🕵🏽‍♀️ Meine Herangehensweise: Vom Problem zur Lösung

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.

💡 Die Lösung: Ein benutzerdefinierter GraphQL-Interceptor

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()
    }
}

📜 Aufschlüsselung

  • Anfragen abfangen: Die Klasse implementiert WebGraphQlInterceptor und dessen Methode intercept(). Diese wird für jede GraphQL-Anfrage aufgerufen.
  • Header extrahieren: Die Funktion 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.
  • Header zum Kontext hinzufügen: Die Methode addHeadersToGraphQLContext() fügt die Header dem Kontext hinzu, sodass sie in allen Resolvern zugänglich sind.

📝 Zugriff auf Header in GraphQL-Resolvern

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

🛠 Vereinfachung des Zugriffs mit benutzerdefinierten Annotationen

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"
        )
    }
}

🌟 Vorteile der Lösung

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:

  • Einfachheit: Die Lösung bietet eine einfache Möglichkeit, Header zu lesen und bereitzustellen.
  • Flexibilität: Es ist möglich, auf jeden beliebigen Header, in allen GraphQL-Resolvern zuzugreifen, ohne zusätzliche Konfigurationen.
  • Vertrautheit: Der Zugriff auf die Header mit @ContextValue bietet eine ähnliche & daher vertraute Lösung für Entwickler, die an Annotationen wie @RequestHeader gewohnt sind.
  • Separation of Concern: Der Interceptor zentralisiert die Logik für die Verarbeitung der Header, was u.a. das Testing & die Wartung erleichtert.

🚀 Fazit

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.


Tags

#spring-boot#softwareentwicklung#clean-code#GraphQL
Previous Article
Wie sich komplexe Switch-Case-Strukturen durch Interfaces und Dependency Injection vermeiden lassen
Marco Schäck

Marco Schäck

Fullstack Entwickler

Inhalte

1
🔍 Das Problem: Zugriff auf Header in GraphQL
2
🕵🏽‍♀️ Meine Herangehensweise: Vom Problem zur Lösung
3
💡 Die Lösung: Ein benutzerdefinierter GraphQL-Interceptor
4
📜 Aufschlüsselung
5
📝 Zugriff auf Header in GraphQL-Resolvern
6
🛠 Vereinfachung des Zugriffs mit benutzerdefinierten Annotationen
7
🌟 Vorteile der Lösung
8
🚀 Fazit

Ähnliche Beiträge

Wie sich komplexe Switch-Case-Strukturen durch Interfaces und Dependency Injection vermeiden lassen
October 08, 2024
3 min
© 2024, All Rights Reserved.

Links

StartseiteLösungenBPM TrainingKarriereInsights

Social Media