Skip to content
Open
Show file tree
Hide file tree
Changes from 122 commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
9960f7e
graphql: a request error is Apollo Server's, in graphql-js's words
nuriyoo Oct 25, 2026
89a61d4
graphql: pin graphql-js's words with its own answers, and take out wh…
nuriyoo Oct 25, 2026
2ed42d3
graphql: a persisted query sent with a query that is not a string is …
nuriyoo Oct 25, 2026
d444ad1
graphql: an operation of a root type the schema lacks is a refusal, n…
nuriyoo Oct 25, 2026
e1f653f
fireflies: the fragment walk's test bounds its time, so a walk once a…
nuriyoo Oct 25, 2026
0112ccb
graphql: requote keeps the text after the first semicolon outside a l…
nuriyoo Oct 25, 2026
79ccda7
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 25, 2026
b5e29f1
linear: introspection is answered without a credential, the 401 is Li…
nuriyoo Oct 25, 2026
4521627
graphql: the engine's docstring names Apollo Server without a major v…
nuriyoo Oct 25, 2026
e1a950d
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 25, 2026
79a15de
graphql: each method, content type and Accept is answered by the laye…
nuriyoo Oct 25, 2026
0940594
graphql: a persisted query's docstring holds what each vendor answere…
nuriyoo Oct 25, 2026
a23fb46
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 25, 2026
265008e
Merge remote-tracking branch 'origin/graphql-a-request-error-is-apoll…
nuriyoo Oct 25, 2026
b34655a
Merge remote-tracking branch 'origin/linear-introspection-and-401-are…
nuriyoo Oct 25, 2026
85de3d7
graphql: a header sent twice, a body of no length and a preflight's h…
nuriyoo Oct 25, 2026
569f2f7
graphql: each docstring and test comment points at the layer that hol…
nuriyoo Oct 25, 2026
7fd2354
graphql: one home for each rule the docstrings state, and present-ten…
nuriyoo Oct 25, 2026
fe255a7
tests: a bare request with no body carries no length, as the measured…
nuriyoo Oct 25, 2026
8f08947
graphql: the ports lose branches no input reaches, and the unit table…
nuriyoo Oct 25, 2026
add05da
graphql: the two Node ports drop the last checks no input reaches
nuriyoo Oct 25, 2026
eb1ed53
tests: the HTTP-layer tables point at the code's dated record rather …
nuriyoo Oct 25, 2026
fcf0257
linear, fireflies: each path is answered by the layer that routes it …
nuriyoo Oct 25, 2026
07786f6
linear, fireflies: one home for the target convention and the body re…
nuriyoo Oct 25, 2026
8e20250
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 26, 2026
d2c3271
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 26, 2026
12d0360
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
7071f7a
linear, fireflies: Cloudflare's TRACE answer closes the connection, a…
nuriyoo Oct 26, 2026
8b9482c
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
71fc46d
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 26, 2026
1214661
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 26, 2026
7465528
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
bbc5e9f
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
91cfdfc
linear, fireflies: the broken-escape refusal's docstring records the …
nuriyoo Oct 26, 2026
8ca5d6e
Merge branch 'main' into graphql-a-request-error-is-apollos
nuriyoo Oct 26, 2026
15ebfdd
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 26, 2026
bbbd780
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
524369d
linear, fireflies: rows for PUT and PATCH at Linear's root, a PROPFIN…
nuriyoo Oct 26, 2026
e0abba0
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
47ef3b9
fireflies: the method set Fastify reads a body for holds the measurem…
nuriyoo Oct 26, 2026
5bd03c6
graphql: Fireflies' 500 is graphql-depth-limit's, and each location c…
nuriyoo Oct 26, 2026
69b8809
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 26, 2026
53187d7
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 26, 2026
6d3b419
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
7a925f9
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
2798146
fireflies: rows for a named operation, two operations' errors, a type…
nuriyoo Oct 26, 2026
ac90a8f
fireflies: the crawl holds no more than the errors it stops at, and t…
nuriyoo Oct 26, 2026
758720b
graphql: the run comment, the depth rule's docstring and a test docst…
nuriyoo Oct 26, 2026
689d0a1
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 26, 2026
48b4dbb
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
4262065
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
79ec35e
linear: the not-found rows sit in the body table, and the router's do…
nuriyoo Oct 26, 2026
637846b
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 26, 2026
c0a27ec
linear, fireflies: an empty Origin is no preflight, Fastify's plain t…
nuriyoo Oct 26, 2026
b240ac1
fireflies: a row for plain text past the limit holding a byte that is…
nuriyoo Oct 26, 2026
3ac023e
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 26, 2026
a933bf3
linear, fireflies: a path is routed once its dot segments are resolve…
nuriyoo Oct 26, 2026
19cb771
linear, fireflies: control rows for a broken escape in the query besi…
nuriyoo Oct 26, 2026
f66b361
linear, fireflies: the broken escape's docstring points at the middle…
nuriyoo Oct 26, 2026
0a9e4bf
docs: Linear's endpoint is each path Express hands Apollo Server once…
nuriyoo Oct 26, 2026
b0bedec
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
5c16437
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
ed59299
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
30eb880
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
74a4fc1
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
58ffa2d
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
30e8a84
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
542c550
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
e177aa4
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
b5c76e7
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
f7df1d5
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
d999ea3
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
b225c74
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
f45e9d4
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
21c1af2
linear tests: _linear_status's docstring points at the table that rea…
nuriyoo Oct 27, 2026
78c3be0
tests: each measured table of the HTTP layer runs as one test
nuriyoo Oct 27, 2026
ac95809
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
3193fc4
linear tests: the rows whose answer without a key turns on what the d…
nuriyoo Oct 27, 2026
257efc8
linear tests: each stage of Apollo Server's that refuses ahead of the…
nuriyoo Oct 27, 2026
a973fbd
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
d2ad248
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
8b774e8
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
ec8c95f
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
73ef323
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
08a2510
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
90f44f4
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
a85ce77
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
8dc2705
fireflies tests: _sent's docstring wraps at the line length
nuriyoo Oct 27, 2026
dc1fc78
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
139245c
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
b347ab1
fireflies: _envelope names every code _FRIENDLY holds
nuriyoo Oct 27, 2026
86b2a47
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
7fe4dda
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
4624cbd
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 27, 2026
9dcffc2
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 27, 2026
be29416
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 27, 2026
9320a05
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 27, 2026
b5b27d0
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 28, 2026
326fc66
graphql: a query a persisted query's hash matches is parsed however e…
nuriyoo Oct 28, 2026
5b3917b
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 28, 2026
6aa5d5e
tests: a hidden by-id root's answer equals an absent one's, body for …
nuriyoo Oct 28, 2026
e5d1ac8
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 28, 2026
b11dda5
tests: Cloudflare's answers ahead of Linear and Fireflies are one tab…
nuriyoo Oct 28, 2026
8052b14
main, tests, NOTICE.md: the table comment points at the middlewares, …
nuriyoo Oct 28, 2026
ce5cc22
graphql: the module docstring names a Refusal's code and message, and…
nuriyoo Oct 28, 2026
8b7d08a
graphql: js_words names the five refusals graphql-js 17 words otherwise
nuriyoo Oct 28, 2026
22e386e
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 28, 2026
722d3ce
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 28, 2026
7c8e02f
Merge branch 'graphql-a-request-error-is-apollos' into linear-introsp…
nuriyoo Oct 28, 2026
270eaf3
Merge branch 'linear-introspection-and-401-are-linears' into graphql-…
nuriyoo Oct 28, 2026
4576a71
Merge branch 'graphql-the-http-layer-is-apollos-and-expresss' into gr…
nuriyoo Oct 28, 2026
a364056
linear, tests: the route docstring describes one route, and the Cloud…
nuriyoo Oct 28, 2026
cf7a758
Merge branch 'main' into graphql-a-request-error-is-apollos
nuriyoo Oct 28, 2026
62f6351
graphql: js_words names each kind of refusal graphql-js 17 answers ot…
nuriyoo Oct 28, 2026
fbb2247
linear: introspection is answered without a credential, the 401 is Li…
nuriyoo Oct 28, 2026
919b6c8
Merge commit 'fbb2247e' into graphql-the-http-layer-is-apollos-and-ex…
nuriyoo Oct 28, 2026
8aa5790
linear, fireflies: each method, content type and Accept is answered b…
nuriyoo Oct 28, 2026
1bad7ec
Merge remote-tracking branch 'origin/main' into graphql-a-request-err…
nuriyoo Oct 28, 2026
f4d2fd9
Merge commit '919b6c8' into graphql-each-path-is-routed-as-the-vendor…
nuriyoo Oct 28, 2026
e7efff6
Merge remote-tracking branch 'origin/graphql-a-request-error-is-apoll…
nuriyoo Oct 28, 2026
ea30e3f
Merge remote-tracking branch 'origin/main' into graphql-each-path-is-…
nuriyoo Oct 28, 2026
467a033
Merge branch 'main' into graphql-each-path-is-routed-as-the-vendor-ro…
nuriyoo Oct 28, 2026
54f6520
Merge remote-tracking branch 'origin/main' into graphql-each-path-is-…
nuriyoo Oct 29, 2026
57041cc
linear, fireflies: a cut escape, a `..` past the root and each dot se…
nuriyoo Oct 29, 2026
e6deb0c
Merge branch 'main' into graphql-each-path-is-routed-as-the-vendor-ro…
nuriyoo Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions backlot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
notion,
oauth,
s3,
sent_path,
slack,
)
from backlot.telemetry import naming, recorder
Expand Down Expand Up @@ -847,6 +848,31 @@ async def serve_a_slashed_hubspot_path_as_the_path_without_it(request: Request,
return await call_next(request)


_GRAPHQL_ENDPOINTS = ("/linear/graphql", "/fireflies/graphql")


def _off_a_graphql_endpoint(scope: Scope) -> bool:
path = scope["path"]
return path.startswith(("/linear/", "/fireflies/")) and path not in _GRAPHQL_ENDPOINTS


@_middleware(_off_a_graphql_endpoint)
async def route_a_linear_or_fireflies_path_as_its_front_does(request: Request, call_next):
"""Route a `/linear` or `/fireflies` path but the endpoint's own to the route that answers it as
the vendor's front does: a `/linear` path Express hands Apollo Server
(``linear.routes_to_apollo``) to ``linear.graphql``, and any other to the vendor's root route
(``linear.express``, ``fireflies.unrouted``), each of which reads the path as sent off the raw
path, which is left alone. Starlette's own routing is not asked: its pattern for the endpoint
takes a path with a newline after it (`/graphql%0A`), and its `path` convertor none with a
newline inside it, where each vendor's front reads both as another path."""
vendor = request.scope["path"].split("/")[1]
served = vendor == "linear" and linear.routes_to_apollo(
linear.express_path(sent_path(request.scope))
)
request.scope["path"] = f"/{vendor}/graphql" if served else f"/{vendor}/"
return await call_next(request)


def _google_route_match(scope: Scope) -> Match:
"""How the best Google route matches this scope: in full, by its path alone (``PARTIAL``, the
router's 405), or not at all. Asked of ``backlot.routers.google.router``, which holds the Google
Expand Down Expand Up @@ -1434,6 +1460,30 @@ async def refuse_a_repeated_header_on_notion_linear_fireflies_and_hubspot(
)


def _a_broken_escape(scope: Scope) -> bool:
return request_target.broken_escape(scope.get("raw_path") or scope["path"].encode())


@_middleware(
lambda scope: (
_at_or_under(_url_path(scope), ("/linear", "/fireflies")) and _a_broken_escape(scope)
)
)
async def refuse_a_broken_escape_on_linear_and_fireflies(request: Request, call_next):
"""Answer a `/linear` or `/fireflies` request whose path holds `%00`, or a `%` two hex digits do
not follow, with :data:`backlot.request_target.CLOUDFLARE_PAGE`'s 400, ahead of
:func:`refuse_cloudflares_methods_on_linear_and_fireflies`. api.linear.app and api.fireflies.ai
answered so `/a%zz` and `/a%00b` by each method of :data:`_CLOUDFLARE_METHOD`, and `/a%2`,
`/graphql%zz` and a `%` before `b` by `POST`, on 2026-10-25 (UTC), and passed `?a=%zz`, `?a=%`,
`?a=%2` and `?a=%00` after `/graphql` on to the vendor. Both closed the connection after the 400
(`POST /graphql%zz`, `POST /a%00b`, `GET /a%2`, 2026-10-26)."""
return Response(
request_target.CLOUDFLARE_PAGE,
status_code=400,
headers={"Content-Type": "text/html", "Connection": "close"},
)


@_middleware(lambda scope: _url_path(scope).startswith("/github"))
async def put_a_request_id_on_github_answers(request: Request, call_next):
"""Put an `x-github-request-id` on every `/github` answer the API gives, as real does on each
Expand Down
14 changes: 14 additions & 0 deletions backlot/request_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,18 @@ def _google(path_kinds: list[str], query_kinds: list[str]) -> Response | None:
return None


#: A `%` with fewer than two hex digits after it.
_BROKEN_ESCAPE = re.compile(rb"%(?![0-9A-Fa-f]{2})")


def broken_escape(path: bytes) -> bool:
"""Whether ``backlot.main.refuse_a_broken_escape_on_linear_and_fireflies`` refuses ``path``.
Cloudflare's page for it comes ahead of its answer to a raw byte: on 2026-10-26 (UTC) both hosts
drew :data:`CLOUDFLARE_PAGE` for `/a%zz` with a raw `é` before or after it or `\\x01` before
it, and for `/a%00` after a raw `é`, where a raw `é` alone drew the empty 400."""
return b"%00" in path or _BROKEN_ESCAPE.search(path) is not None


def _cloudflare(scope: Scope, path_kinds: list[str], query_kinds: list[str]) -> Response | None:
""":data:`CLOUDFLARE_PAGE` where a `nul` or a `cr` is anywhere, and where `Authorization` or
one of the five conditional headers comes more than once
Expand Down Expand Up @@ -315,6 +327,8 @@ def answer(scope: Scope) -> Response | None:
return _google(in_path, in_query)
if errors.atlassian.owns(path):
return _atlassian(scope, in_path, in_query)
if any(_under(path, p) for p in ("/linear", "/fireflies")) and broken_escape(sent_path):
return _page(CLOUDFLARE_PAGE, "text/html")
if any(_under(path, p) for p in ("/notion", "/hubspot", "/linear", "/fireflies")):
return _cloudflare(scope, in_path, in_query)
if errors.s3.owns(path):
Expand Down
9 changes: 9 additions & 0 deletions backlot/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@
from __future__ import annotations

from fastapi import Request
from starlette.types import Scope


def sent_path(scope: Scope) -> str:
"""The path a client sends the vendor's own host: this request's as it arrived, escapes and
all, with the first segment, which names the vendor here, taken off. A first segment with no
slash after it leaves `/`."""
raw = (scope.get("raw_path") or scope["path"].encode()).decode("latin-1")
return "/" + raw.partition("/")[2].partition("/")[2]


async def json_body(request: Request) -> dict:
Expand Down
87 changes: 65 additions & 22 deletions backlot/routers/fireflies.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Fireflies.ai's GraphQL API, served at ``/fireflies/graphql``.

Fireflies is **GraphQL only** — there is no REST surface to emulate — so this router is one
endpoint. The schema and the resolvers live in ``backlot/graphql/`` (``fireflies.graphql`` +
``fireflies_resolvers.py``); everything here is HTTP: a request and credentials in, a GraphQL
envelope out, or the answer of a layer in front of Apollo Server's run, in the order
:func:`graphql` lists them.
endpoint, and what answers at any other path (:func:`unrouted`). The schema and the resolvers live
in ``backlot/graphql/`` (``fireflies.graphql`` + ``fireflies_resolvers.py``); everything here is
HTTP: a request and credentials in, a GraphQL envelope out, or the answer of a layer in front of
Apollo Server's run, in the order :func:`graphql` lists them.

Auth is the bearer path (``Authorization: Bearer <api_key>``), which is what the vendor's own
quickstart shows for all four of its raw-HTTP examples, read by
Expand All @@ -20,6 +20,8 @@

from __future__ import annotations

import re
import urllib.parse
from typing import Any

from fastapi import APIRouter, Request
Expand All @@ -39,6 +41,7 @@
from backlot.graphql import apollo_http, engine
from backlot.graphql import body as request_body
from backlot.graphql.fireflies_resolvers import build_engine
from backlot.routers import sent_path

router = APIRouter(prefix="/fireflies", tags=["fireflies"])

Expand Down Expand Up @@ -121,18 +124,20 @@ def _auth_failed(seen: dict[str, str], message: str) -> Response:
"object_not_found": True,
}

# The methods Fastify reads a body for and has no route for at this path: it reads the body, then
# answers its not-found handler's 404 (:func:`_not_found`).
_READ_THEN_NOT_FOUND = ("DELETE", "PATCH", "PUT")
# The methods Fastify reads a body for but `OPTIONS`, which `@fastify/cors` answers first: at a path
# or under a method no route takes, it reads the body, then answers its not-found handler's 404
# (:func:`_not_found`). Any other method's body goes unread: a `PROPFIND` to `/graphql/` under
# `a/b/c` or with `{` as JSON drew the 404 on 2026-10-26 (UTC).
_BODIED = ("DELETE", "PATCH", "POST", "PUT")


@router.api_route("/graphql", methods=list(apollo_http.METHODS), include_in_schema=False)
async def graphql(request: Request):
"""The single Fireflies endpoint, in front of which Fireflies runs Fastify and
``@fastify/cors``. Not in the OpenAPI document on purpose: this is a GraphQL service, and
describing one route that accepts an arbitrary query would tell an OpenAPI→MCP bridge nothing
useful (hence no ``SOURCE_PREFIXES`` entry either — see ``backlot/openapi.py``). Introspection
is the schema description for this endpoint.
"""Fireflies' endpoint, the one path Fastify routes, in front of which Fireflies runs nginx
(:func:`nginx_passes`), Fastify and ``@fastify/cors``. Not in the OpenAPI document on purpose:
this is a GraphQL service, and describing one route that accepts an arbitrary query would
tell an OpenAPI→MCP bridge nothing useful (hence no ``SOURCE_PREFIXES`` entry either — see
``backlot/openapi.py``). Introspection is the schema description for this endpoint.

The layers answer in this order, each measured against api.fireflies.ai on 2026-10-25 (UTC)
with the one after it wrong too: ``@fastify/cors`` (:func:`_preflight`), Fastify
Expand All @@ -143,7 +148,7 @@ async def graphql(request: Request):
if method == "OPTIONS":
return _preflight(request)
if method not in ("GET", "HEAD", "POST"):
if method in _READ_THEN_NOT_FOUND:
if method in _BODIED:
read = await _fastify_read(request, routed=False)
if isinstance(read, Response):
return read
Expand Down Expand Up @@ -238,19 +243,55 @@ def _preflight(request: Request) -> Response:


def _not_found(request: Request) -> Response:
"""Fireflies' 404 for a method its route does not take, the method and the path quoted in it as
a client puts them after ``https://api.fireflies.ai``, the query string as sent.
"""Fireflies' 404 for a method or a path no route takes, the method, the path as a client sent
it (:func:`~backlot.routers.sent_path`) and the query string as sent quoted in it.
api.fireflies.ai answered so each of the 28 methods of :data:`apollo_http.METHODS` but GET,
HEAD, POST, OPTIONS and Cloudflare's three on 2026-10-25 (UTC), `PUT`, `DELETE` and `PATCH` once
Fastify had read their body (:func:`_fastify_read`), and quoted `/graphql?x=%7B%20y&z&a=b+c` as
sent."""
# The path as sent, but for the segment that names Fireflies here.
path = "/" + request.scope["raw_path"].decode("latin-1").split("/", 2)[2]
HEAD, POST, OPTIONS and Cloudflare's three at `/graphql` on 2026-10-25 (UTC), `PUT`, `DELETE`
and `PATCH` once Fastify had read their body (:func:`_fastify_read`), and quoted
`/graphql?x=%7B%20y&z&a=b+c` as sent; and `POST`, `GET`, `HEAD`, `PUT`, `PROPFIND` and `BIND`
at `/GRAPHQL`, `/graphql/x`, `//graphql` and `/graphql%2F`, `POST` at `/%2Fgraphql` and
`/graphql%0A`, and `PUT` at `/graph%71l`, quoting each path as sent."""
query = request.scope["query_string"].decode("latin-1")
message = f"Cannot {request.method} {path}{'?' if query else ''}{query}"
message = f"Cannot {request.method} {sent_path(request.scope)}{'?' if query else ''}{query}"
return _body_refusal(message, 404, "not_found")


_NGINX_404 = (
b"<html>\r\n<head><title>404 Not Found</title></head>\r\n<body>\r\n"
b"<center><h1>404 Not Found</h1></center>\r\n<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"
)
_NGINX_LOCATION = re.compile(rb"/graphql(/|$)", re.IGNORECASE)


def nginx_passes(path: str) -> bool:
"""Whether nginx in front of Fastify passes on ``path``, as a client sent it
(:func:`~backlot.routers.sent_path`): once each escape is decoded and each run of slashes is
one, it starts with `/graphql`, in any case, followed by a slash, by nothing or by a newline
that ends it. api.fireflies.ai answered with Fastify's 404 at each path :func:`_not_found`
names, and with nginx's (:data:`_NGINX_404`) at `/graphqlx`, `/graphql.json`, `/graphql;x`,
`/graphql%20`, `/graphql%7E`, `/x/graphql` and `/`, on 2026-10-25 (UTC)."""
decoded = re.sub(rb"/{2,}", b"/", urllib.parse.unquote_to_bytes(path))
return _NGINX_LOCATION.match(decoded) is not None


@router.api_route("/", methods=list(apollo_http.METHODS), include_in_schema=False)
async def unrouted(request: Request):
"""Fireflies' answer at a path Fastify has no route for, each of which ``backlot.main`` routes
here: :data:`_NGINX_404` where nginx keeps the request (:func:`nginx_passes`), and Fastify's
where it passes it on, :func:`_preflight` to an `OPTIONS` and its not-found handler's 404
(:func:`_not_found`) to any other method, after reading the body of one of :data:`_BODIED`
(:func:`_fastify_read`)."""
if not nginx_passes(sent_path(request.scope)):
return Response(_NGINX_404, status_code=404, headers={"content-type": "text/html"})
if request.method == "OPTIONS":
return _preflight(request)
if request.method in _BODIED:
read = await _fastify_read(request, routed=False)
if isinstance(read, Response):
return read
return _not_found(request)


_FASTIFY_PARSED = ("application/json", "text/plain", "application/x-www-form-urlencoded")


Expand All @@ -267,7 +308,9 @@ async def _fastify_read(request: Request, routed: bool) -> Any:
api.fireflies.ai answered so each body sent on 2026-10-25 (UTC) to `POST`, `PUT`, `DELETE`
and `PATCH`: no type with a body was the 415 by `POST` and the 404 by `PUT`, `a/b/c` and an
empty type the 415 by both, `application/graphql` the 415 by `POST`, past the limit too, and
the 404 by `PUT`, and a form or plain text past the limit the 413 by both."""
the 404 by `PUT`, and a form or plain text past the limit the 413 by both. A `POST` to
`/graphql/`, which no route takes, was read as a `PUT` to `/graphql` is: `{` and an empty body
drew the 400s, `a/b/c` the 415, and plain text, `application/graphql` and no type the 404."""
content_type = request.headers.get("content-type")
if content_type is None:
length = request.headers.get("content-length")
Expand Down
Loading
Loading