SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

api: SPA fallback sert les shells pré-rendus ; ETag/Cache-Control sur /openapi.json

Simon-Pierre Boucher committed 20 days ago (Sep 5, 2026) parent 43e7f08

1 changed file +23 −3

modified hfmarketdata/api/main.py +23 −3
@@ -32,7 +32,7 @@ from pathlib import Path
32 32 import duckdb
33 33 from fastapi import FastAPI, HTTPException, Query
34 34 from fastapi.middleware.cors import CORSMiddleware
35 −from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
35 +from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, Response
36 36 from fastapi.staticfiles import StaticFiles
37 37
38 38 from core import errors as _errors
@@ -499,6 +499,22 @@ for _mod in V2_MODULES:
499 499 _log.exception("module %s failed to load: %s", _mod, e)
500 500
501 501
502 +# ETag on the OpenAPI document so the docs' If-None-Match revalidation is cheap
503 +import hashlib as _hashlib
504 +
505 +@app.middleware("http")
506 +async def _openapi_etag(request, call_next):
507 + response = await call_next(request)
508 + if request.url.path == "/openapi.json" and response.status_code == 200:
509 + body = b"".join([chunk async for chunk in response.body_iterator])
510 + etag = '"' + _hashlib.sha256(body).hexdigest()[:32] + '"'
511 + if request.headers.get("if-none-match") == etag:
512 + return Response(status_code=304, headers={"ETag": etag, "Cache-Control": "public, max-age=300"})
513 + return Response(body, status_code=200, media_type="application/json",
514 + headers={"ETag": etag, "Cache-Control": "public, max-age=300"})
515 + return response
516 +
517 +
502 518 # ------------------------------------------------------------------------------
503 519 # Static React platform (mounted last so it doesn't shadow the API) + SPA fallback
504 520 # ------------------------------------------------------------------------------
@@ -510,9 +526,13 @@ if WEB_DIST.is_dir():
510 526 def spa(path: str):
511 527 if path.startswith(("v1/", "health", "openapi", "docs/", "redoc")) and not path.startswith("docs/"):
512 528 raise HTTPException(404, "Not found")
529 + root = str(WEB_DIST.resolve())
513 530 candidate = (WEB_DIST / path).resolve()
514 − if path and candidate.is_file() and str(candidate).startswith(str(WEB_DIST.resolve())):
515 − return FileResponse(candidate)
531 + if path and str(candidate).startswith(root):
532 + # exact file, prerendered shell (<route>.html or <route>/index.html), else the SPA entry
533 + for c in (candidate, candidate.with_suffix(".html") if candidate.suffix == "" else None, candidate / "index.html"):
534 + if c is not None and c.is_file():
535 + return FileResponse(c)
516 536 return FileResponse(WEB_DIST / "index.html")
517 537
518 538
519 539