Skip to content

Commit 18e04ca

Browse files
feat: check of server and client version in response header (#1443)
Fixes #1074 Check of version headers in the response has been added to the client side. --------- Co-authored-by: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com>
1 parent b75bb25 commit 18e04ca

4 files changed

Lines changed: 77 additions & 4 deletions

File tree

src/blueapi/client/rest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from collections.abc import Callable, Mapping
23
from typing import Any, Literal, TypeVar
34

@@ -10,6 +11,7 @@
1011
)
1112
from pydantic import BaseModel, TypeAdapter, ValidationError
1213

14+
from blueapi import __version__
1315
from blueapi.config import RestConfig
1416
from blueapi.service.authentication import JWTAuth, SessionManager
1517
from blueapi.service.model import (
@@ -32,6 +34,8 @@
3234

3335
TRACER = get_tracer("rest")
3436

37+
LOGGER = logging.getLogger(__name__)
38+
3539

3640
class UnauthorisedAccessError(Exception):
3741
pass
@@ -271,6 +275,17 @@ def _request_and_deserialize(
271275
raise exception
272276
if response.status_code == status.HTTP_204_NO_CONTENT:
273277
raise NoContentError(target_type)
278+
if (server_version := response.headers.get("x-blueapi-version")) is not None:
279+
from packaging.version import Version
280+
281+
if (server_version := Version(server_version).base_version) != (
282+
client_version := Version(__version__).base_version
283+
):
284+
LOGGER.warning(
285+
f"Version mismatch: Blueapi server version is {server_version} "
286+
f"but client version is {client_version}. "
287+
f"Some features may not work as expected."
288+
)
274289
deserialized = TypeAdapter(target_type).validate_python(response.json())
275290
return deserialized
276291

src/blueapi/service/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from starlette.responses import JSONResponse
3535
from super_state_machine.errors import TransitionError
3636

37+
from blueapi import __version__
3738
from blueapi.config import ApplicationConfig, OIDCConfig, Tag
3839
from blueapi.service import interface
3940
from blueapi.worker import TrackableTask, WorkerState
@@ -123,7 +124,7 @@ def get_app(config: ApplicationConfig):
123124
app.include_router(secure_router, dependencies=dependencies)
124125
app.add_exception_handler(KeyError, on_key_error_404)
125126
app.add_exception_handler(jwt.PyJWTError, on_token_error_401)
126-
app.middleware("http")(add_api_version_header)
127+
app.middleware("http")(add_version_headers)
127128
app.middleware("http")(inject_propagated_observability_context)
128129
app.middleware("http")(log_request_details)
129130
if config.api.cors:
@@ -568,11 +569,12 @@ def start(config: ApplicationConfig):
568569
)
569570

570571

571-
async def add_api_version_header(
572+
async def add_version_headers(
572573
request: Request, call_next: Callable[[Request], Awaitable[Response]]
573574
):
574575
response = await call_next(request)
575576
response.headers["X-API-Version"] = ApplicationConfig.REST_API_VERSION
577+
response.headers["X-BlueAPI-Version"] = __version__
576578
return response
577579

578580

tests/unit_tests/client/test_rest.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import uuid
22
from pathlib import Path
3-
from unittest.mock import Mock, patch
3+
from unittest.mock import MagicMock, Mock, patch
44

55
import pytest
66
import requests
77
import responses
8+
from packaging.version import Version
89

10+
from blueapi import __version__
911
from blueapi.client.rest import (
1012
BlueapiRestClient,
1113
BlueskyRemoteControlError,
@@ -196,3 +198,36 @@ def test_parameter_error_other_string():
196198
input=34,
197199
)
198200
assert str(p1) == "Invalid value 34 for field field_one.0: error_message"
201+
202+
203+
@pytest.mark.parametrize(
204+
"server_version,logging_warning_present",
205+
[(__version__, False), ("0.0.1", True), (None, False)],
206+
)
207+
@patch("blueapi.client.rest.TypeAdapter")
208+
@patch("blueapi.client.rest.requests.Session.request")
209+
@patch("blueapi.client.rest.LOGGER")
210+
def test_server_and_client_versions(
211+
mock_logger: MagicMock,
212+
mock_request: Mock,
213+
mock_type_adapter: Mock,
214+
rest: BlueapiRestClient,
215+
server_version: str,
216+
logging_warning_present: bool,
217+
):
218+
response = Mock(spec=requests.Response)
219+
response.status_code = 200
220+
response.headers = {"x-blueapi-version": server_version}
221+
mock_request.return_value = response
222+
223+
rest.get_plans()
224+
225+
if logging_warning_present:
226+
mock_logger.warning.assert_called_once_with(
227+
f"Version mismatch: Blueapi server version is "
228+
f"{Version(server_version).base_version} "
229+
f"but client version is {Version(__version__).base_version}. "
230+
f"Some features may not work as expected."
231+
)
232+
else:
233+
mock_logger.assert_not_called()

tests/unit_tests/service/test_main.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,28 @@
55
from fastapi import FastAPI, Request
66
from fastapi.testclient import TestClient
77

8-
from blueapi.service.main import get_passthrough_headers, log_request_details
8+
from blueapi import __version__
9+
from blueapi.config import ApplicationConfig
10+
from blueapi.service.main import (
11+
add_version_headers,
12+
get_passthrough_headers,
13+
log_request_details,
14+
)
15+
16+
17+
async def test_add_version_header():
18+
app = FastAPI()
19+
app.middleware("http")(add_version_headers)
20+
21+
@app.get("/")
22+
async def root():
23+
return {"message": "Hello World"}
24+
25+
client = TestClient(app)
26+
response = client.get("/")
27+
28+
assert response.headers["X-API-VERSION"] == ApplicationConfig.REST_API_VERSION
29+
assert response.headers["X-BlueAPI-VERSION"] == __version__
930

1031

1132
async def test_log_request_details():

0 commit comments

Comments
 (0)