Skip to content

Commit 27e5473

Browse files
[GPCAPIM-419]-[Required Missing Header Not Rejected (Content-Type)]-[RP]
1 parent 483e095 commit 27e5473

9 files changed

Lines changed: 96 additions & 8 deletions

File tree

gateway-api/openapi.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,30 @@ paths:
238238
diagnostics:
239239
type: string
240240
example: "Patient not found"
241+
'415':
242+
description: Unsupported Media Type - Content-Type header must be "application/fhir+json"
243+
content:
244+
application/fhir+json:
245+
schema:
246+
type: object
247+
properties:
248+
resourceType:
249+
type: string
250+
example: "OperationOutcome"
251+
issue:
252+
type: array
253+
items:
254+
type: object
255+
properties:
256+
severity:
257+
type: string
258+
example: "error"
259+
code:
260+
type: string
261+
example: "invalid"
262+
diagnostics:
263+
type: string
264+
example: 'Unsupported "Content-Type". Expected "application/fhir+json".'
241265
'500':
242266
description: Internal server error
243267
content:

gateway-api/src/gateway_api/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def get_structured_record() -> Response:
119119
log_error(e)
120120
response.add_error_response(e)
121121
except Exception:
122-
error = UnexpectedError(traceback=traceback.format_exc())
122+
error = UnexpectedError()
123123
log_error(error)
124124
response.add_error_response(error)
125125

gateway-api/src/gateway_api/common/error.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
from dataclasses import dataclass
2-
from http.client import BAD_GATEWAY, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND
2+
from http.client import (
3+
BAD_GATEWAY,
4+
BAD_REQUEST,
5+
INTERNAL_SERVER_ERROR,
6+
NOT_FOUND,
7+
UNSUPPORTED_MEDIA_TYPE,
8+
)
39

410
from fhir.stu3 import Issue, IssueCode, IssueSeverity, OperationOutcome
511

@@ -110,7 +116,14 @@ class JWTValidationError(AbstractCDGError):
110116

111117

112118
class UnexpectedError(AbstractCDGError):
113-
_message = "Internal Server Error: {traceback}"
119+
_message = "Internal Server Error"
114120
status_code = INTERNAL_SERVER_ERROR
115121
severity = IssueSeverity.ERROR
116122
error_code = IssueCode.EXCEPTION
123+
124+
125+
class UnsupportedMediaTypeError(AbstractCDGError):
126+
_message = "Unsupported Media Type"
127+
status_code = UNSUPPORTED_MEDIA_TYPE
128+
severity = IssueSeverity.ERROR
129+
error_code = IssueCode.INVALID

gateway-api/src/gateway_api/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ def text(self) -> str:
7575

7676
def create_mock_request(headers: dict[str, str], body: dict[str, Any]) -> Request:
7777
"""Create a proper Flask Request object with headers and JSON body."""
78+
content_type = headers.get("Content-Type", "application/fhir+json")
7879
builder = EnvironBuilder(
7980
method="POST",
8081
path="/patient/$gpc.getstructuredrecord",
8182
data=json.dumps(body),
82-
content_type="application/fhir+json",
83+
content_type=content_type,
8384
headers=headers,
8485
)
8586
env = builder.get_environ()

gateway-api/src/gateway_api/get_structured_record/request.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
from gateway_api.common.error import (
1111
InvalidRequestJSONError,
1212
MissingOrEmptyHeaderError,
13+
UnsupportedMediaTypeError,
1314
)
1415

16+
ACCEPTED_CONTENT_TYPE = "application/fhir+json"
17+
1518
# Access record structured interaction ID from
1619
# https://developer.nhs.uk/apis/gpconnect/accessrecord_structured_development.html#spine-interactions
1720
ACCESS_RECORD_STRUCTURED_INTERACTION_ID = (
@@ -32,15 +35,25 @@ class GetStructuredRecordRequest:
3235
def __init__(self, request: Request) -> None:
3336
self._http_request = request
3437
self._headers = CaseInsensitiveDict(request.headers)
38+
self._validate_content_type()
3539
try:
36-
self.parameters = Parameters.model_validate(request.get_json())
40+
self.parameters = Parameters.model_validate(
41+
request.get_json(silent=True, force=True)
42+
)
3743
except (BadRequest, ValidationError) as error:
3844
raise InvalidRequestJSONError() from error
3945

4046
self._status_code: int | None = None
4147

4248
self._validate_headers()
4349

50+
def _validate_content_type(self) -> None:
51+
content_type = self._headers.get("Content-Type")
52+
if content_type is None:
53+
return
54+
if content_type.split(";")[0].strip().lower() != ACCEPTED_CONTENT_TYPE:
55+
raise UnsupportedMediaTypeError()
56+
4457
@property
4558
def trace_id(self) -> str:
4659
trace_id: str = self._headers["Ssp-TraceID"]

gateway-api/src/gateway_api/get_structured_record/test_request.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from gateway_api.common.error import (
77
MissingOrEmptyHeaderError,
8+
UnsupportedMediaTypeError,
89
)
910
from gateway_api.conftest import create_mock_request
1011
from gateway_api.get_structured_record.request import GetStructuredRecordRequest
@@ -118,3 +119,33 @@ def test_raises_value_error_when_trace_id_header_is_whitespace(
118119
match='Missing or empty required header "Ssp-TraceID"',
119120
):
120121
GetStructuredRecordRequest(request=mock_request)
122+
123+
def test_missing_content_type_header_is_accepted(
124+
self, valid_simple_request_payload: dict[str, Any]
125+
) -> None:
126+
"""Test that a missing Content-Type header does not raise an error."""
127+
headers = {
128+
"Content-Type": "",
129+
"Ssp-TraceID": "test-trace-id",
130+
"ODS-from": "test-ods",
131+
}
132+
mock_request = create_mock_request(headers, valid_simple_request_payload)
133+
134+
GetStructuredRecordRequest(request=mock_request)
135+
136+
def test_raises_unsupported_media_type_when_content_type_is_invalid(
137+
self, valid_simple_request_payload: dict[str, Any]
138+
) -> None:
139+
"""
140+
Test that UnsupportedMediaTypeError is raised when Content-Type
141+
is not "application/fhir+json".
142+
"""
143+
headers = {
144+
"Content-Type": "application/json",
145+
"Ssp-TraceID": "test-trace-id",
146+
"ODS-from": "test-ods",
147+
}
148+
mock_request = create_mock_request(headers, valid_simple_request_payload)
149+
150+
with pytest.raises(UnsupportedMediaTypeError):
151+
GetStructuredRecordRequest(request=mock_request)

gateway-api/src/gateway_api/get_structured_record/test_response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def test_add_provider_response_adds_200_status(
6262
assert actual == 200, f"Expected status code to be 200, but got {actual}"
6363

6464
def test_add_error_response_adds_error_response_body(self) -> None:
65-
error = UnexpectedError(traceback="something broke")
65+
error = UnexpectedError()
6666

6767
response = GetStructuredRecordResponse()
6868
response.add_error_response(error)
@@ -73,7 +73,7 @@ def test_add_error_response_adds_error_response_body(self) -> None:
7373
{
7474
"severity": "error",
7575
"code": "exception",
76-
"diagnostics": "Internal Server Error: something broke",
76+
"diagnostics": "Internal Server Error",
7777
}
7878
],
7979
}

gateway-api/tests/schema/test_openapi_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_api_schema_compliance(case: Case, base_url: str) -> None:
5353
# GPCAPIM-421
5454
schemathesis.checks.not_a_server_error,
5555
# GPCAPIM-419
56-
schemathesis.checks.missing_required_header,
56+
# schemathesis.checks.missing_required_header,
5757
# GPCAPIM-422
5858
schemathesis.checks.unsupported_method,
5959
],

schemathesis.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
[generation]
22
mode = "all"
3+
4+
[checks.missing_required_header]
5+
expected-statuses = [400]
6+
7+
[checks.negative_data_rejection]
8+
expected-statuses = [400, 401, 403, 404, 406, 415, 422, 428, "5xx"]

0 commit comments

Comments
 (0)