From c467cd811bfcc2584c68b36dd72cdc3232b96c70 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes <65790536+dan-fernandes@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:23:01 +0000 Subject: [PATCH 01/11] Set app's docs_url to "/" --- src/blueapi/service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index ba5d022d2..6440724bd 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -132,7 +132,7 @@ async def inner(app: FastAPI): def get_app(config: ApplicationConfig): app = FastAPI( - docs_url="/docs", + docs_url="/", title="BlueAPI Control", summary="BlueAPI wraps bluesky plans and devices and " "exposes endpoints to send commands/receive data", From d1ce0b9362265c0931151619f9d8b4812dada169 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Tue, 13 Jan 2026 10:59:00 +0000 Subject: [PATCH 02/11] Revert "Set app's docs_url to "/"" This reverts commit c467cd811bfcc2584c68b36dd72cdc3232b96c70. --- src/blueapi/service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 6440724bd..ba5d022d2 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -132,7 +132,7 @@ async def inner(app: FastAPI): def get_app(config: ApplicationConfig): app = FastAPI( - docs_url="/", + docs_url="/docs", title="BlueAPI Control", summary="BlueAPI wraps bluesky plans and devices and " "exposes endpoints to send commands/receive data", From 288aa942768d2477b8ca63786c2123e9cc3e7ac9 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Tue, 13 Jan 2026 11:05:16 +0000 Subject: [PATCH 03/11] Add redirect from / to /docs --- src/blueapi/service/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index ba5d022d2..14b33e5d5 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -205,6 +205,15 @@ async def on_token_error_401(_: Request, __: Exception): ) +@secure_router.get("/", include_in_schema=False) +def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: + """Redirect to docs url""" + return RedirectResponse( + status_code=status.HTTP_308_PERMANENT_REDIRECT, + url="/docs", + ) + + @secure_router.get("/environment", tags=[Tag.ENV]) @start_as_current_span(TRACER, "runner") def get_environment( From 3cfc3e9c3e7f31dc3ca072ca464c27db6e11fee5 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Tue, 13 Jan 2026 11:22:55 +0000 Subject: [PATCH 04/11] Add docs_endpoint config field --- src/blueapi/config.py | 4 ++++ src/blueapi/service/main.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/blueapi/config.py b/src/blueapi/config.py index eb1b3122e..164f5bc47 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -165,6 +165,10 @@ class CORSConfig(BlueapiBaseModel): class RestConfig(BlueapiBaseModel): url: HttpUrl = HttpUrl("http://localhost:8000") cors: CORSConfig | None = None + docs_endpoint: str = Field( + description="Relative url of the OpenAPI docs endpoint", + default="/docs", + ) class ScratchRepository(BlueapiBaseModel): diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 14b33e5d5..936ebf601 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -132,7 +132,7 @@ async def inner(app: FastAPI): def get_app(config: ApplicationConfig): app = FastAPI( - docs_url="/docs", + docs_url=config.api.docs_endpoint, title="BlueAPI Control", summary="BlueAPI wraps bluesky plans and devices and " "exposes endpoints to send commands/receive data", @@ -208,9 +208,11 @@ async def on_token_error_401(_: Request, __: Exception): @secure_router.get("/", include_in_schema=False) def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: """Redirect to docs url""" + config = runner.run(interface.config) + return RedirectResponse( status_code=status.HTTP_308_PERMANENT_REDIRECT, - url="/docs", + url=config.api.docs_endpoint, ) From 24bda6f2ee913fd60dce28def66b9fc60e907f44 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Tue, 13 Jan 2026 11:29:25 +0000 Subject: [PATCH 05/11] Update config schema --- helm/blueapi/config_schema.json | 6 ++++++ helm/blueapi/values.schema.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/helm/blueapi/config_schema.json b/helm/blueapi/config_schema.json index d54bc5357..71c37fdaf 100644 --- a/helm/blueapi/config_schema.json +++ b/helm/blueapi/config_schema.json @@ -373,6 +373,12 @@ } ], "default": null + }, + "docs_endpoint": { + "default": "/docs", + "description": "Relative url of the OpenAPI docs endpoint", + "title": "Docs Endpoint", + "type": "string" } }, "title": "RestConfig", diff --git a/helm/blueapi/values.schema.json b/helm/blueapi/values.schema.json index 1578c0dad..6cfd38550 100644 --- a/helm/blueapi/values.schema.json +++ b/helm/blueapi/values.schema.json @@ -789,6 +789,12 @@ } ] }, + "docs_endpoint": { + "title": "Docs Endpoint", + "description": "Relative url of the OpenAPI docs endpoint", + "default": "/docs", + "type": "string" + }, "url": { "title": "Url", "default": "http://localhost:8000/", From 418788ccd01a570e6317ab7918a0d713e235558e Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:01:39 +0000 Subject: [PATCH 06/11] fix config interface --- src/blueapi/service/main.py | 4 +--- tests/unit_tests/service/test_rest_api.py | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 936ebf601..be219fcb6 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -208,11 +208,9 @@ async def on_token_error_401(_: Request, __: Exception): @secure_router.get("/", include_in_schema=False) def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: """Redirect to docs url""" - config = runner.run(interface.config) - return RedirectResponse( status_code=status.HTTP_308_PERMANENT_REDIRECT, - url=config.api.docs_endpoint, + url=interface.config().api.docs_endpoint, ) diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index 0c731723f..4bbaff1b5 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -744,6 +744,15 @@ def test_logout( ) +def test_docs_redirect( + mock_authn_server, + client_with_auth: TestClient, +): + client_with_auth.follow_redirects = False + response = client_with_auth.get("/") + assert response.headers.get("location") == RestConfig().docs_endpoint + + @pytest.mark.parametrize("has_oidc_config", [True, False]) def test_logout_when_oidc_config_invalid( has_oidc_config: bool, From abe53934ff37bdcd8607f0be04eedc09d6f9c8bb Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Wed, 14 Jan 2026 10:13:24 +0000 Subject: [PATCH 07/11] Change docs endpoint from config field to constant variable --- helm/blueapi/config_schema.json | 6 ------ helm/blueapi/values.schema.json | 6 ------ src/blueapi/config.py | 4 ---- src/blueapi/service/main.py | 6 +++--- tests/unit_tests/service/test_rest_api.py | 2 +- 5 files changed, 4 insertions(+), 20 deletions(-) diff --git a/helm/blueapi/config_schema.json b/helm/blueapi/config_schema.json index 71c37fdaf..d54bc5357 100644 --- a/helm/blueapi/config_schema.json +++ b/helm/blueapi/config_schema.json @@ -373,12 +373,6 @@ } ], "default": null - }, - "docs_endpoint": { - "default": "/docs", - "description": "Relative url of the OpenAPI docs endpoint", - "title": "Docs Endpoint", - "type": "string" } }, "title": "RestConfig", diff --git a/helm/blueapi/values.schema.json b/helm/blueapi/values.schema.json index 6cfd38550..1578c0dad 100644 --- a/helm/blueapi/values.schema.json +++ b/helm/blueapi/values.schema.json @@ -789,12 +789,6 @@ } ] }, - "docs_endpoint": { - "title": "Docs Endpoint", - "description": "Relative url of the OpenAPI docs endpoint", - "default": "/docs", - "type": "string" - }, "url": { "title": "Url", "default": "http://localhost:8000/", diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 164f5bc47..eb1b3122e 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -165,10 +165,6 @@ class CORSConfig(BlueapiBaseModel): class RestConfig(BlueapiBaseModel): url: HttpUrl = HttpUrl("http://localhost:8000") cors: CORSConfig | None = None - docs_endpoint: str = Field( - description="Relative url of the OpenAPI docs endpoint", - default="/docs", - ) class ScratchRepository(BlueapiBaseModel): diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index be219fcb6..328e9b1a1 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -71,6 +71,7 @@ VENDOR_CONTEXT_HEADER = "tracestate" AUTHORIZAITON_HEADER = "authorization" PROPAGATED_HEADERS = {CONTEXT_HEADER, VENDOR_CONTEXT_HEADER, AUTHORIZAITON_HEADER} +DOCS_ENDPOINT = "/docs" class Tag(str, Enum): @@ -132,7 +133,7 @@ async def inner(app: FastAPI): def get_app(config: ApplicationConfig): app = FastAPI( - docs_url=config.api.docs_endpoint, + docs_url=DOCS_ENDPOINT, title="BlueAPI Control", summary="BlueAPI wraps bluesky plans and devices and " "exposes endpoints to send commands/receive data", @@ -209,8 +210,7 @@ async def on_token_error_401(_: Request, __: Exception): def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: """Redirect to docs url""" return RedirectResponse( - status_code=status.HTTP_308_PERMANENT_REDIRECT, - url=interface.config().api.docs_endpoint, + status_code=status.HTTP_308_PERMANENT_REDIRECT, url=DOCS_ENDPOINT ) diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index 4bbaff1b5..1cc9fab4d 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -750,7 +750,7 @@ def test_docs_redirect( ): client_with_auth.follow_redirects = False response = client_with_auth.get("/") - assert response.headers.get("location") == RestConfig().docs_endpoint + assert response.headers.get("location") == main.DOCS_ENDPOINT @pytest.mark.parametrize("has_oidc_config", [True, False]) From 9e108120fa474f880a25ae8e49c47daea2039cf8 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Wed, 14 Jan 2026 10:23:49 +0000 Subject: [PATCH 08/11] Change root redirect from permanent to temporary redirect --- src/blueapi/service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index 328e9b1a1..d2168f615 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -210,7 +210,7 @@ async def on_token_error_401(_: Request, __: Exception): def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: """Redirect to docs url""" return RedirectResponse( - status_code=status.HTTP_308_PERMANENT_REDIRECT, url=DOCS_ENDPOINT + status_code=status.HTTP_307_TEMPORARY_REDIRECT, url=DOCS_ENDPOINT ) From 439ca4fe3494d843b1f7e4a938f43bd689857866 Mon Sep 17 00:00:00 2001 From: Daniel Fernandes <65790536+dan-fernandes@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:52:55 +0000 Subject: [PATCH 09/11] Update tests/unit_tests/service/test_rest_api.py Add status code assertion to redirection test Co-authored-by: Zoheb Shaikh --- tests/unit_tests/service/test_rest_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index 1cc9fab4d..88baea01c 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -751,7 +751,7 @@ def test_docs_redirect( client_with_auth.follow_redirects = False response = client_with_auth.get("/") assert response.headers.get("location") == main.DOCS_ENDPOINT - + assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT @pytest.mark.parametrize("has_oidc_config", [True, False]) def test_logout_when_oidc_config_invalid( From 15944f4270c8b7e559efb4885e1ff47087d103ec Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Wed, 14 Jan 2026 11:15:46 +0000 Subject: [PATCH 10/11] Fix root_redirect function signature --- src/blueapi/service/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/service/main.py b/src/blueapi/service/main.py index d2168f615..9711f2005 100644 --- a/src/blueapi/service/main.py +++ b/src/blueapi/service/main.py @@ -207,7 +207,7 @@ async def on_token_error_401(_: Request, __: Exception): @secure_router.get("/", include_in_schema=False) -def root_redirect(runner: Annotated[WorkerDispatcher, Depends(_runner)]) -> Response: +def root_redirect() -> RedirectResponse: """Redirect to docs url""" return RedirectResponse( status_code=status.HTTP_307_TEMPORARY_REDIRECT, url=DOCS_ENDPOINT From c126751549fd1d9077dd11d6676fe8d5e25999ec Mon Sep 17 00:00:00 2001 From: Daniel Fernandes Date: Wed, 14 Jan 2026 11:17:38 +0000 Subject: [PATCH 11/11] Lint --- tests/unit_tests/service/test_rest_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/service/test_rest_api.py b/tests/unit_tests/service/test_rest_api.py index 88baea01c..b0bc09dd3 100644 --- a/tests/unit_tests/service/test_rest_api.py +++ b/tests/unit_tests/service/test_rest_api.py @@ -753,6 +753,7 @@ def test_docs_redirect( assert response.headers.get("location") == main.DOCS_ENDPOINT assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT + @pytest.mark.parametrize("has_oidc_config", [True, False]) def test_logout_when_oidc_config_invalid( has_oidc_config: bool,