From 1f3a29e7f5d75be123af7cb235c054697623466e Mon Sep 17 00:00:00 2001 From: Tom Kralidis Date: Wed, 1 Apr 2026 23:15:29 -0400 Subject: [PATCH] OAProc: ensure binary outputs are not UTF-8 decoded (#2304) --- pygeoapi/process/hello_world.py | 17 ++++++++++++++++- pygeoapi/process/manager/base.py | 5 +++-- tests/api/test_processes.py | 14 +++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/pygeoapi/process/hello_world.py b/pygeoapi/process/hello_world.py index 4577929aa..cd9d3192d 100644 --- a/pygeoapi/process/hello_world.py +++ b/pygeoapi/process/hello_world.py @@ -3,7 +3,7 @@ # Authors: Tom Kralidis # Francesco Martinelli # -# Copyright (c) 2022 Tom Kralidis +# Copyright (c) 2026 Tom Kralidis # Copyright (c) 2024 Francesco Martinelli # # Permission is hereby granted, free of charge, to any person @@ -29,6 +29,7 @@ # # ================================================================= +import json import logging from pygeoapi.process.base import BaseProcessor, ProcessorExecuteError @@ -82,6 +83,17 @@ 'minOccurs': 0, 'maxOccurs': 1, 'keywords': ['message'] + }, + 'as_bytes': { + 'title': 'As bytes', + 'description': 'Whether to force return as bytes', + 'schema': { + 'type': 'bool', + 'default': False + }, + 'minOccurs': 0, + 'maxOccurs': 1, + 'keywords': ['as_bytes'] } }, 'outputs': { @@ -136,6 +148,9 @@ def execute(self, data, outputs=None): 'value': value } + if data.get('as_bytes', False): + json.dumps(produced_outputs).encode('utf-8') + return mimetype, produced_outputs def __repr__(self): diff --git a/pygeoapi/process/manager/base.py b/pygeoapi/process/manager/base.py index fadb90fbd..632ce4ea2 100644 --- a/pygeoapi/process/manager/base.py +++ b/pygeoapi/process/manager/base.py @@ -4,7 +4,7 @@ # Ricardo Garcia Silva # Francesco Martinelli # -# Copyright (c) 2024 Tom Kralidis +# Copyright (c) 2026 Tom Kralidis # (c) 2023 Ricardo Garcia Silva # (c) 2026 Francesco Martinelli # @@ -277,7 +277,8 @@ def _execute_handler_sync(self, p: BaseProcessor, job_id: str, current_status = JobStatus.running jfmt, outputs = p.execute(data_dict, **extra_execute_parameters) - if isinstance(outputs, bytes): + if isinstance(outputs, bytes) and outputs.isascii(): + LOGGER.debug('output is ASCII; decoding utf-8') outputs = outputs.decode('utf-8') if requested_response == RequestedResponse.document.value: diff --git a/tests/api/test_processes.py b/tests/api/test_processes.py index 9d837747c..7f2e3ddaa 100644 --- a/tests/api/test_processes.py +++ b/tests/api/test_processes.py @@ -95,7 +95,7 @@ def test_describe_processes(config, api_): assert process['title'] == 'Hello World' assert len(process['keywords']) == 3 assert len(process['links']) == 6 - assert len(process['inputs']) == 2 + assert len(process['inputs']) == 3 assert len(process['outputs']) == 1 assert len(process['outputTransmission']) == 1 assert len(process['jobControlOptions']) == 2 @@ -242,6 +242,12 @@ def test_execute_process(config, api_): 'name': 'Test document' } } + req_body_10 = { + 'inputs': { + 'name': 'Test document as bytes response', + 'as_bytes': True + } + } cleanup_jobs = set() @@ -410,6 +416,12 @@ def test_execute_process(config, api_): response2 = '{"id":"echo","value":"Hello Test document!"}' assert response == response2 + req = mock_api_request(data=req_body_10) + rsp_headers, code, response = execute_process(api_, req, 'hello-world') + + response2 = '{"id":"echo","value":"Hello Test document as bytes response!"}' # noqa + assert response == response2 + # Cleanup time.sleep(2) # Allow time for any outstanding async jobs for _, job_id in cleanup_jobs: