From 98b0ea9dfbac4d04779c82c0f5223d4ed9c16cf0 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 11:51:06 +0100 Subject: [PATCH 01/15] notifications-utils: 121.0.0 -> 124.2.0, adapt use of LetterTimings --- .pre-commit-config.yaml | 2 +- app/models.py | 4 ++-- requirements.in | 2 +- requirements.txt | 4 ++-- requirements_for_test.txt | 4 ++-- requirements_for_test_common.in | 2 +- ruff.toml | 2 +- uv.toml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5add126c02..b401caf171 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -# This file was automatically copied from notifications-utils@121.0.0 +# This file was automatically copied from notifications-utils@124.2.0 repos: - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/app/models.py b/app/models.py index ff80d12185..aea7207b35 100644 --- a/app/models.py +++ b/app/models.py @@ -5,7 +5,7 @@ from flask import current_app, url_for from jsonschema import ValidationError, validate from notifications_utils.insensitive_dict import InsensitiveDict -from notifications_utils.letter_timings import get_letter_timings +from notifications_utils.letter_timings import LetterTimings from notifications_utils.recipient_validation.email_address import validate_email_address from notifications_utils.recipient_validation.errors import InvalidRecipientError from notifications_utils.recipient_validation.phone_number import PhoneNumber @@ -1824,7 +1824,7 @@ def serialize(self) -> SerializedNotification: serialized["postcode"], ) = (personalisation.get(line) for line in address_lines_1_to_6_and_postcode_keys) - serialized["estimated_delivery"] = get_letter_timings( + serialized["estimated_delivery"] = LetterTimings( serialized["created_at"], postage=self.postage ).latest_delivery.strftime(DATETIME_FORMAT) diff --git a/requirements.in b/requirements.in index a80067ea16..125eda31f0 100644 --- a/requirements.in +++ b/requirements.in @@ -25,7 +25,7 @@ psutil~=6.0 notifications-python-client~=12.1 # Run `make bump-utils` to update to the latest version -notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@121.0.0 +notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@124.2.0 git+https://github.com/alphagov/gds_metrics_python.git@6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72 diff --git a/requirements.txt b/requirements.txt index 437a37f358..8648c9a69d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o requirements.txt --exclude-newer 7d +# uv pip compile requirements.in -o requirements.txt alembic==1.18.4 \ --hash=sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a \ --hash=sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc @@ -833,7 +833,7 @@ mistune==0.8.4 \ notifications-python-client==12.1.0 \ --hash=sha256:bc44987acbb72de4cded700447d0d246c020dcd0f0311ff2ca884ce1c4001b76 # via -r requirements.in -notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@c202643dda528216441a4cac3f7cf2797088d33f +notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@ace9443c2800adc8107f3bb8ac32e483724612d6 # via -r requirements.in opentelemetry-api==1.42.1 \ --hash=sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714 \ diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 664398e4aa..ca73df8965 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements_for_test.in -o requirements_for_test.txt --exclude-newer 7d +# uv pip compile requirements_for_test.in -o requirements_for_test.txt alembic==1.18.4 \ --hash=sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a \ --hash=sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc @@ -1196,7 +1196,7 @@ mypy-extensions==1.1.0 \ notifications-python-client==12.1.0 \ --hash=sha256:bc44987acbb72de4cded700447d0d246c020dcd0f0311ff2ca884ce1c4001b76 # via -r requirements.txt -notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@c202643dda528216441a4cac3f7cf2797088d33f +notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@ace9443c2800adc8107f3bb8ac32e483724612d6 # via -r requirements.txt opentelemetry-api==1.42.1 \ --hash=sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714 \ diff --git a/requirements_for_test_common.in b/requirements_for_test_common.in index d13499fa18..aac3baed2c 100644 --- a/requirements_for_test_common.in +++ b/requirements_for_test_common.in @@ -1,4 +1,4 @@ -# This file was automatically copied from notifications-utils@121.0.0 +# This file was automatically copied from notifications-utils@124.2.0 beautifulsoup4 pytest diff --git a/ruff.toml b/ruff.toml index 3694d3136c..47e059b1b8 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,4 @@ -# This file was automatically copied from notifications-utils@121.0.0 +# This file was automatically copied from notifications-utils@124.2.0 extend-exclude = [ "migrations/versions/", diff --git a/uv.toml b/uv.toml index 7b06d1be2e..093c58ceb9 100644 --- a/uv.toml +++ b/uv.toml @@ -1,4 +1,4 @@ -# This file was automatically copied from notifications-utils@121.0.0 +# This file was automatically copied from notifications-utils@124.2.0 exclude-newer = "7 days" From 7c9481b82fb3394128b368ead31c742e6c59bffb Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Tue, 21 Jul 2026 16:02:54 +0100 Subject: [PATCH 02/15] flask: use FlaskRelaxedContainerJSONProvider as json_provider_class --- app/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index 0316aabc8a..8c14484953 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,6 +9,7 @@ from celery import current_task from flask import ( + Flask, current_app, g, has_request_context, @@ -27,6 +28,7 @@ from notifications_utils.clients.signing.signing_client import Signing from notifications_utils.clients.zendesk.zendesk_client import ZendeskClient from notifications_utils.eventlet import EventletTimeout +from notifications_utils.json import FlaskRelaxedContainerJSONProvider from notifications_utils.local_vars import LazyLocalGetter from notifications_utils.logging import flask as utils_logging from sqlalchemy import event @@ -159,9 +161,11 @@ zendesk_client = LocalProxy(get_zendesk_client) -def create_app(application): +def create_app(application: Flask) -> Flask: from app.config import Config, configs + application.json_provider_class = FlaskRelaxedContainerJSONProvider + notify_environment = os.environ["NOTIFY_ENVIRONMENT"] if notify_environment in configs: From 169b257c57f75947099c7945bcdd2ee097fbf269 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Tue, 21 Jul 2026 16:04:33 +0100 Subject: [PATCH 03/15] letter previews: use RelaxedContainerJSONEncoder to encode json payloads to template-preview --- app/template/rest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/template/rest.py b/app/template/rest.py index cb01e54d1d..9129251980 100644 --- a/app/template/rest.py +++ b/app/template/rest.py @@ -5,6 +5,7 @@ from flask import Blueprint, current_app, jsonify, request from flask.ctx import has_request_context from notifications_utils import SMS_CHAR_COUNT_LIMIT +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.pdf import extract_page_from_pdf from notifications_utils.template import ( LetterPreviewTemplate, @@ -338,7 +339,14 @@ def _get_png_preview_or_overlaid_pdf(url, data, notification_id, json=True): headers.update(request.get_onwards_request_headers()) if json: - resp = requests_post(url, json=data, headers=headers) + resp = requests_post( + url, + data=RCJSONEncoder().encode(data), + headers={ + "Content-Type": "application/json", + **headers, + }, + ) else: resp = requests_post(url, data=data, headers=headers) From 51f4538d8fd667916783c933d05d28f21896c12c Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:41:20 +0100 Subject: [PATCH 04/15] MMGClient: use RelaxedContainerJSONEncoder to encode json payloads --- app/clients/sms/mmg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/clients/sms/mmg.py b/app/clients/sms/mmg.py index 3ffc48e4f3..c65bbb32f7 100644 --- a/app/clients/sms/mmg.py +++ b/app/clients/sms/mmg.py @@ -1,6 +1,7 @@ import json import requests +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from app.clients.sms import SmsClient, SmsClientResponseException from app.otel_metrics.provider import record_request_duration @@ -100,7 +101,7 @@ def try_send_sms(self, to, content, reference, international, sender): response = self.requests_session.request( "POST", self.mmg_url, - data=json.dumps(data), + data=RCJSONEncoder().encode(data), headers={"Content-Type": "application/json", "Authorization": f"Basic {self.api_key}"}, timeout=10, ) From 47bd4e34645251386bd1c26be43bb231df4a7798 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:41:46 +0100 Subject: [PATCH 05/15] DVLAClient: use RelaxedContainerJSONEncoder to encode json payloads --- app/clients/letter/dvla.py | 50 +++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/app/clients/letter/dvla.py b/app/clients/letter/dvla.py index 4e9036d449..843a64b98b 100644 --- a/app/clients/letter/dvla.py +++ b/app/clients/letter/dvla.py @@ -10,6 +10,7 @@ import jwt import requests from flask import current_app +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.recipient_validation.postal_address import PostalAddress from requests.adapters import HTTPAdapter from urllib3.util.ssl_ import create_urllib3_context @@ -153,10 +154,13 @@ def _handle_401(e: requests.HTTPError): with _handle_common_dvla_errors(custom_httperror_exc_handler=_handle_401): response = self.session.post( f"{self.base_url}/thirdparty-access/v1/authenticate", - json={ - "userName": self.dvla_username.get(), - "password": self.dvla_password.get(), - }, + headers={"Content-Type": "application/json"}, + data=RCJSONEncoder().encode( + { + "userName": self.dvla_username.get(), + "password": self.dvla_password.get(), + } + ), ) response.raise_for_status() @@ -212,11 +216,14 @@ def _handle_401(e: requests.HTTPError): with _handle_common_dvla_errors(custom_httperror_exc_handler=_handle_401): response = self.session.post( f"{self.base_url}/thirdparty-access/v1/password", - json={ - "userName": self.dvla_username.get(), - "password": self.dvla_password.get(), - "newPassword": new_password, - }, + headers={"Content-Type": "application/json"}, + data=RCJSONEncoder().encode( + { + "userName": self.dvla_username.get(), + "password": self.dvla_password.get(), + "newPassword": new_password, + } + ), ) response.raise_for_status() @@ -283,16 +290,21 @@ def _handle_http_errors(e: requests.HTTPError): with _handle_common_dvla_errors(custom_httperror_exc_handler=_handle_http_errors): response = self.session.post( f"{self.base_url}/print-request/v1/print/jobs", - headers=self._get_auth_headers(), - json=self._format_create_print_job_json( - notification_id=notification_id, - reference=reference, - address=address, - postage=postage, - service_id=service_id, - organisation_id=organisation_id, - pdf_file=pdf_file, - callback_url=callback_url, + headers={ + "Content-Type": "application/json", + **self._get_auth_headers(), + }, + data=RCJSONEncoder().encode( + self._format_create_print_job_json( + notification_id=notification_id, + reference=reference, + address=address, + postage=postage, + service_id=service_id, + organisation_id=organisation_id, + pdf_file=pdf_file, + callback_url=callback_url, + ) ), ) response.raise_for_status() From 56693c14864e273b8a943d0cda7615bac78e18da Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:42:05 +0100 Subject: [PATCH 06/15] DocumentDownloadClient: use RelaxedContainerJSONEncoder to encode json payloads --- app/clients/document_download.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/clients/document_download.py b/app/clients/document_download.py index a8083ad25f..f3fd1237aa 100644 --- a/app/clients/document_download.py +++ b/app/clients/document_download.py @@ -1,6 +1,7 @@ import requests from flask import current_app, request from flask.ctx import has_request_context +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder class DocumentDownloadError(Exception): @@ -61,14 +62,17 @@ def upload_document( if filename: data["filename"] = filename - headers = {"Authorization": f"Bearer {self.auth_token}"} + headers = { + "Authorization": f"Bearer {self.auth_token}", + "Content-Type": "application/json", + } if has_request_context() and hasattr(request, "get_onwards_request_headers"): headers.update(request.get_onwards_request_headers()) response = self.requests_session.post( self._get_upload_url(service_id), headers=headers, - json=data, + data=RCJSONEncoder().encode(data), ) response.raise_for_status() From d97a89c1ecb8770d9d3a5a78c107456f820ec1e0 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:42:32 +0100 Subject: [PATCH 07/15] service callbacks: use RelaxedContainerJSONEncoder to encode json payloads --- app/celery/service_callback_tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/celery/service_callback_tasks.py b/app/celery/service_callback_tasks.py index c5dfc9ac48..508cfe4ab7 100644 --- a/app/celery/service_callback_tasks.py +++ b/app/celery/service_callback_tasks.py @@ -1,10 +1,10 @@ -import json import logging from contextvars import ContextVar from datetime import datetime import requests from flask import current_app +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.local_vars import LazyLocalGetter from werkzeug.local import LocalProxy @@ -160,7 +160,7 @@ def _send_data_to_service_callback_api(self, data, service_callback_url, token, response = requests_session.request( method="POST", url=service_callback_url, - data=json.dumps(data), + data=RCJSONEncoder().encode(data), headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"}, timeout=5, ) From 5a5d8b77bd03e1c4bfd3c1e24c0aa4355f4a9f38 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:45:08 +0100 Subject: [PATCH 08/15] research mode: use RelaxedContainerJSONEncoder to encode json payloads of fake letter receipts --- app/celery/research_mode_tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/celery/research_mode_tasks.py b/app/celery/research_mode_tasks.py index f04b445db7..ad69fa4482 100644 --- a/app/celery/research_mode_tasks.py +++ b/app/celery/research_mode_tasks.py @@ -5,6 +5,7 @@ import requests from flask import current_app, jsonify +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.local_vars import LazyLocalGetter from notifications_utils.timezones import local_timezone from werkzeug.local import LocalProxy @@ -80,7 +81,9 @@ def send_letter_response(notification_id: uuid.UUID, billable_units: int, postag data = _create_fake_letter_callback_data(notification_id, billable_units, postage) try: - response = requests_session.request("POST", api_call, headers=headers, data=json.dumps(data), timeout=30) # type: ignore[attr-defined] + response = requests_session.request( # type: ignore[attr-defined] + "POST", api_call, headers=headers, data=RCJSONEncoder().encode(data), timeout=30 + ) response.raise_for_status() except requests.HTTPError as e: current_app.logger.error( From ce87e64b2a11aaf289fe911b479a517131269910 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:46:29 +0100 Subject: [PATCH 09/15] research mode: use RelaxedContainerJSONEncoder to encode json payloads of fake MMG receipts --- app/celery/research_mode_tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/celery/research_mode_tasks.py b/app/celery/research_mode_tasks.py index ad69fa4482..a7420c262c 100644 --- a/app/celery/research_mode_tasks.py +++ b/app/celery/research_mode_tasks.py @@ -182,7 +182,7 @@ def mmg_callback(notification_id, to): else: status = "3" - return json.dumps( + return RCJSONEncoder().encode( { "reference": "mmg_reference", "CID": str(notification_id), From 514af16c4c74577966bbab55d5fcc9c4a5b62f55 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:52:52 +0100 Subject: [PATCH 10/15] research mode: use RelaxedContainerJSONEncoder to encode parts of fake SES receipts --- app/celery/research_mode_tasks.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/celery/research_mode_tasks.py b/app/celery/research_mode_tasks.py index a7420c262c..8cce7e04bd 100644 --- a/app/celery/research_mode_tasks.py +++ b/app/celery/research_mode_tasks.py @@ -1,4 +1,3 @@ -import json import uuid from contextvars import ContextVar from datetime import datetime @@ -271,7 +270,7 @@ def ses_notification_callback(reference): "MessageId": "8e83c020-1234-1234-1234-92a8ee9baa0a", "TopicArn": "arn:aws:sns:eu-west-1:12341234:ses_notifications", "Subject": None, - "Message": json.dumps(ses_message_body), + "Message": RCJSONEncoder().encode(ses_message_body), "Timestamp": uniform_timestamp, "SignatureVersion": "1", "Signature": "[REDACTED]", @@ -340,7 +339,7 @@ def _ses_bounce_callback(reference, bounce_type): "MessageId": "36e67c28-1234-1234-1234-2ea0172aa4a7", "TopicArn": "arn:aws:sns:eu-west-1:12341234:ses_notifications", "Subject": None, - "Message": json.dumps(ses_message_body), + "Message": RCJSONEncoder().encode(ses_message_body), "Timestamp": uniform_timestamp, "SignatureVersion": "1", "Signature": "[REDACTED]", From f4bee53e9e66bf195287cb76bd61642d5a390d78 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 13:58:45 +0100 Subject: [PATCH 11/15] process_ses_results: use RelaxedContainerJSONEncoder to encode json into log messages --- app/celery/process_ses_receipts_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/celery/process_ses_receipts_tasks.py b/app/celery/process_ses_receipts_tasks.py index 049010ea9c..b4960a9964 100644 --- a/app/celery/process_ses_receipts_tasks.py +++ b/app/celery/process_ses_receipts_tasks.py @@ -4,6 +4,7 @@ from celery import Task from celery.exceptions import Retry from flask import current_app, json +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from sqlalchemy.orm.exc import NoResultFound from app import notify_celery @@ -98,7 +99,7 @@ def process_ses_results( # noqa: C901 notification.id, extra={ "notification_id": notification.id, - "bounce_message": json.dumps(bounce_message), + "bounce_message": RCJSONEncoder().encode(bounce_message), "bounced_at": delivery_dt, "bounced_ago": (uniform_now - delivery_dt).total_seconds() if delivery_dt is not None else None, "bounce_message_type": bounce_message_bounce.get("bounceType"), From 80f07924a58ebc948b86d0b4c031dc087a3f62f3 Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 14:13:04 +0100 Subject: [PATCH 12/15] report requests: use RelaxedContainerJSONEncoder to encode json log parameters --- app/service/rest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/service/rest.py b/app/service/rest.py index 5a4db481ec..9f7aef55f1 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -1,10 +1,10 @@ import itertools -import json import uuid from datetime import datetime from uuid import UUID from flask import Blueprint, current_app, jsonify, request +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.letter_timings import ( letter_can_be_cancelled, too_late_to_cancel_letter, @@ -1594,7 +1594,7 @@ def create_report_request_by_type(service_id): extra = { "user_id": existing_request.user_id, "service_id": existing_request.service_id, - "report_request_parameter": json.dumps(existing_request.parameter, separators=(",", ":")), + "report_request_parameter": RCJSONEncoder(separators=(",", ":")).encode(existing_request.parameter), "report_request_id": existing_request.id, } current_app.logger.info( @@ -1613,7 +1613,7 @@ def create_report_request_by_type(service_id): "report_request_id": created_request.id, "user_id": created_request.user_id, "service_id": created_request.service_id, - "report_request_parameter": json.dumps(created_request.parameter, separators=(",", ":")), + "report_request_parameter": RCJSONEncoder(separators=(",", ":")).encode(created_request.parameter), } current_app.logger.info( "Report request %(report_request_id)s for user %(user_id)s (service %(service_id)s) " From 841fe018f849b7f3e1e102a11913236685b5ff2f Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 14:21:32 +0100 Subject: [PATCH 13/15] letters: use RelaxedContainerJSONEncoder to encode json for s3 object metadata --- app/letters/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/letters/utils.py b/app/letters/utils.py index 03a418042c..eb06e6501f 100644 --- a/app/letters/utils.py +++ b/app/letters/utils.py @@ -1,5 +1,4 @@ import io -import json import math from datetime import datetime, timedelta from enum import Enum @@ -7,6 +6,7 @@ import boto3 from flask import current_app from notifications_utils.clients.redis import daily_limit_cache_key +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.letter_timings import LETTER_PROCESSING_DEADLINE from notifications_utils.pdf import pdf_page_count from notifications_utils.s3 import s3upload @@ -156,7 +156,7 @@ def move_scan_to_invalid_pdf_bucket(source_filename, message=None, invalid_pages if message: metadata["message"] = message if invalid_pages: - metadata["invalid_pages"] = json.dumps(invalid_pages) + metadata["invalid_pages"] = RCJSONEncoder().encode(invalid_pages) if page_count: metadata["page_count"] = str(page_count) From fecaaf3c680d46db28ea76b91a81895e610c3d9b Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 16:20:20 +0100 Subject: [PATCH 14/15] schema validation: use RelaxedContainerJSONEncoder to encode json for error messages --- app/schema_validation/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/schema_validation/__init__.py b/app/schema_validation/__init__.py index 7379d6b229..d51c804ac6 100644 --- a/app/schema_validation/__init__.py +++ b/app/schema_validation/__init__.py @@ -1,9 +1,9 @@ -import json import re from datetime import datetime, timedelta from uuid import UUID from jsonschema import Draft7Validator, FormatChecker, ValidationError +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.recipient_validation.email_address import validate_email_address from notifications_utils.recipient_validation.errors import InvalidEmailError, InvalidPhoneError from notifications_utils.recipient_validation.phone_number import PhoneNumber @@ -144,7 +144,7 @@ def build_error_message(errors): fields.append({"error": "ValidationError", "message": field}) message = {"status_code": 400, "errors": unique_errors(fields)} - return json.dumps(message) + return RCJSONEncoder().encode(message) def unique_errors(dups): From f1fa1aacb222e33b203b2a86a1df71d44cd1329b Mon Sep 17 00:00:00 2001 From: Robert Scott Date: Thu, 23 Jul 2026 16:23:12 +0100 Subject: [PATCH 15/15] user management url tokens: use RelaxedContainerJSONEncoder to encode json part --- app/user/rest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/user/rest.py b/app/user/rest.py index d2fb854ce3..80123adf8a 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -1,9 +1,9 @@ -import json import uuid from datetime import datetime from urllib.parse import urlencode from flask import Blueprint, abort, current_app, jsonify, request +from notifications_utils.json import RelaxedContainerJSONEncoder as RCJSONEncoder from notifications_utils.recipient_validation.phone_number import PhoneNumber from sqlalchemy.exc import IntegrityError @@ -589,7 +589,7 @@ def get_organisations_and_services_for_user(user_id): def _create_reset_password_url(email, next_redirect, base_url=None): - data = json.dumps({"email": email, "created_at": str(datetime.utcnow())}) + data = RCJSONEncoder().encode({"email": email, "created_at": str(datetime.utcnow())}) static_url_part = "/new-password/" full_url = url_with_token(data, static_url_part, base_url=base_url) if next_redirect: @@ -598,19 +598,19 @@ def _create_reset_password_url(email, next_redirect, base_url=None): def _create_verification_url(user, base_url): - data = json.dumps({"user_id": str(user.id), "email": user.email_address}) + data = RCJSONEncoder().encode({"user_id": str(user.id), "email": user.email_address}) url = "/verify-email/" return url_with_token(data, url, base_url=base_url) def _create_confirmation_url(user, email_address): - data = json.dumps({"user_id": str(user.id), "email": email_address}) + data = RCJSONEncoder().encode({"user_id": str(user.id), "email": email_address}) url = "/your-account/email/confirm/" return url_with_token(data, url) def _create_2fa_url(user, secret_code, next_redirect, email_auth_link_host): - data = json.dumps({"user_id": str(user.id), "secret_code": secret_code}) + data = RCJSONEncoder().encode({"user_id": str(user.id), "secret_code": secret_code}) url = "/email-auth/" full_url = url_with_token(data, url, base_url=email_auth_link_host) if next_redirect: