From 5cb34eff44316a7ce5cf6bd6279f7d67df2f293e Mon Sep 17 00:00:00 2001 From: Leonard Lu Date: Fri, 6 Feb 2026 12:26:11 -0500 Subject: [PATCH] Fix GraphiQL IDE rendering (#137) - Fix Flask views using Jinja2's render_template_string which HTML-escaped JSON values, breaking the GraphiQL JS config. Both sync and async views now use to_template_string() with the framework-agnostic simple_renderer. - Fix operationName not passed to the template due to a variable naming mismatch (operationName vs operation_name) in the sync Flask view. - Restore the locationQuery JS function accidentally removed in 578453f, which caused a ReferenceError when editing queries in the GraphiQL IDE. - Escape < and > as \u003c and \u003e in tojson() to prevent queries containing from breaking the page by prematurely closing the script tag. - Add CodeMirror 5 fold gutter CSS to fix broken fold markers in the GraphiQL editor. --- RELEASE.md | 20 +++++++ src/graphql_server/flask/views.py | 14 ++--- src/graphql_server/http/__init__.py | 6 ++- src/graphql_server/static/graphiql.html | 16 ++++++ src/tests/http/test_graphql_ide.py | 71 ++++++++++++++++++++++--- uv.lock | 4 +- 6 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..d37aa1c --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,20 @@ +Release type: patch + +Fix GraphiQL IDE rendering issues introduced in v3.0.0: + +- Fix Flask views using Jinja2's `render_template_string` which + HTML-escaped JSON values (e.g., `"` instead of `"`), breaking + the GraphiQL JavaScript configuration. Both sync and async Flask + views now use `to_template_string()` with the framework-agnostic + `simple_renderer`. +- Fix `operationName` not being passed to the GraphiQL template due + to a variable naming mismatch (`operationName` vs `operation_name`) + in the sync Flask view. +- Restore the `locationQuery` JavaScript function that was + accidentally removed, which caused a `ReferenceError` when editing + queries, variables, or headers in the GraphiQL IDE. +- Escape `<` and `>` as `\u003c` and `\u003e` in `tojson()` to + prevent queries containing `` from breaking the GraphiQL + page by prematurely closing the script tag. +- Add CodeMirror 5 fold gutter CSS to fix missing/broken fold + markers in the GraphiQL editor. diff --git a/src/graphql_server/flask/views.py b/src/graphql_server/flask/views.py index b637b5e..2976aad 100644 --- a/src/graphql_server/flask/views.py +++ b/src/graphql_server/flask/views.py @@ -11,7 +11,7 @@ ) from typing_extensions import TypeGuard -from flask import Request, Response, render_template_string, request +from flask import Request, Response, request from flask.views import View from graphql_server.http import GraphQLRequestData from graphql_server.http.async_base_view import ( @@ -138,12 +138,8 @@ def dispatch_request(self) -> ResponseReturnValue: def render_graphql_ide( self, request: Request, request_data: GraphQLRequestData ) -> Response: - return render_template_string( - self.graphql_ide_html, - query=request_data.query, - variables=request_data.variables, - operationName=request_data.operation_name, - ) # type: ignore + content = request_data.to_template_string(self.graphql_ide_html) + return Response(content, status=200, content_type="text/html") class AsyncFlaskHTTPRequestAdapter(AsyncHTTPRequestAdapter): @@ -208,9 +204,7 @@ async def dispatch_request(self) -> ResponseReturnValue: # type: ignore async def render_graphql_ide( self, request: Request, request_data: GraphQLRequestData ) -> Response: - content = render_template_string( - self.graphql_ide_html, **request_data.to_template_context() - ) + content = request_data.to_template_string(self.graphql_ide_html) return Response(content, status=200, content_type="text/html") def is_websocket_request(self, request: Request) -> TypeGuard[Request]: diff --git a/src/graphql_server/http/__init__.py b/src/graphql_server/http/__init__.py index 2af8274..b48d94c 100644 --- a/src/graphql_server/http/__init__.py +++ b/src/graphql_server/http/__init__.py @@ -37,7 +37,11 @@ def process_result( def tojson(value): if value not in ["true", "false", "null", "undefined"]: value = json.dumps(value) - # value = escape_js_value(value) + # Escape characters that are significant to the HTML parser when + # embedded inside " + query_encoded = quote(query) + response = await http_client.get( + f"/graphql?query={query_encoded}", + headers={"Accept": "text/html"}, + ) + + assert response.status_code == 200 + # The < and > in the query must be escaped as \u003c and \u003e so the + # HTML parser doesn't see a literal and close the tag early. + assert "\\u003c/script\\u003e" in response.text diff --git a/uv.lock b/uv.lock index e7870f6..ebe5978 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <4.0" resolution-markers = [ "python_full_version >= '3.13'", @@ -1284,7 +1284,7 @@ wheels = [ [[package]] name = "graphql-server" -version = "3.0.0b8" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "graphql-core" },