Copilot commented on code in PR #79: URL: https://github.com/apache/openserverless-runtimes/pull/79#discussion_r3180617576
########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + + # Parse status code from status string (e.g., "200 OK" -> 200) + status_code = 200 + if response_status: + try: + status_code = int(response_status.split()[0]) + except (ValueError, IndexError): + status_code = 200 + + return { + "statusCode": status_code, + "headers": headers_dict, + "body": response_body.decode('utf-8') if isinstance(response_body, bytes) else response_body, + } + +while True: + line = stdin.readline() + if not line: break + args = json.loads(line) + + reset_environ() + res = {} + + try: + stderr.write("="*80 + "\n") + stderr.write("DEBUG: NEW REQUEST\n") + stderr.write("="*80 + "\n") + stderr.write(f"DEBUG: Incoming args: {json.dumps(args, indent=2)}\n") + stderr.write("-"*80 + "\n") Review Comment: This launcher logs the full incoming args and derived request parameters for every invocation. In production this can leak secrets/PII from headers/body into logs and can significantly increase log volume; please gate this behind an opt-in env var (e.g., `OW_DEBUG`) or remove/limit it (redact auth headers, truncate bodies). ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + + # Parse status code from status string (e.g., "200 OK" -> 200) + status_code = 200 + if response_status: + try: + status_code = int(response_status.split()[0]) + except (ValueError, IndexError): + status_code = 200 + + return { + "statusCode": status_code, + "headers": headers_dict, + "body": response_body.decode('utf-8') if isinstance(response_body, bytes) else response_body, + } + +while True: + line = stdin.readline() + if not line: break + args = json.loads(line) + + reset_environ() + res = {} + + try: + stderr.write("="*80 + "\n") + stderr.write("DEBUG: NEW REQUEST\n") + stderr.write("="*80 + "\n") + stderr.write(f"DEBUG: Incoming args: {json.dumps(args, indent=2)}\n") + stderr.write("-"*80 + "\n") + + # Initialize collections + other_params = {} + + # Parse the input arguments to build the WSGI environ + if "value" in args and isinstance(args["value"], dict): + stderr.write("DEBUG: Processing value fields...\n") + for k, v in args["value"].items(): + if k == "PREFERRED_URL_SCHEME": + environ["wsgi.url_scheme"] = v + + elif k == "API_URL": + environ["API_URL"] = v + + elif k == "__ow_method": + environ["REQUEST_METHOD"] = v.upper() + stderr.write(f"DEBUG: ✓ Method set to: {v.upper()}\n") + + elif k == "__ow_path": + environ["PATH_INFO"] = v + stderr.write(f"DEBUG: ✓ Path set to: {v}\n") + + elif k == "__ow_headers": + if isinstance(v, dict): + stderr.write(f"DEBUG: Processing headers: {list(v.keys())}\n") + for k, v in v.items(): + if k.lower() == "x-scheme": environ["wsgi.url_scheme"] = v + else: environ[k.upper().replace("-", "_")] = v Review Comment: WSGI expects incoming HTTP headers in `environ` to be provided as `HTTP_<NAME>` (except `CONTENT_TYPE`/`CONTENT_LENGTH`). Mapping headers to bare names (e.g., `ACCEPT`, `HOST`) will prevent Flask/Werkzeug from seeing them correctly. Consider converting `__ow_headers` into `HTTP_...` keys, while special-casing `Content-Type` and `Content-Length`. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] Review Comment: `start_response` should accept the optional third parameter `exc_info=None` (WSGI spec). Some middleware/framework paths call `start_response(status, headers, exc_info)`; with the current signature those requests will crash. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", Review Comment: `wsgi.url_scheme` should be lower-case `"http"`/`"https"` per WSGI/PEP 3333. Setting it to `"HTTP"` can break URL generation and request handling in Werkzeug. ########## packages/python/flask/app.py: ########## @@ -0,0 +1,43 @@ +#--web true +#--docker https://registry.hub.docker.com/94lama/python:flask + +import json +from flask import Flask, request +from hello import hello + +app = Flask(__name__) + [email protected]("/") +def home(): + return "Hello, app!" + [email protected]("/post", methods=["POST"]) +def post(): + if request.is_json: + return request.json + elif request.form: + return dict(request.form) + else: + return request.data.decode('utf-8') + [email protected]("/put", methods=["PUT"]) +def put(): + if request.is_json: + return request.json + elif request.form: + return dict(request.form) + else: + return request.data.decode('utf-8') + [email protected]("/delete", methods=["DELETE"]) +def delete(): + if request.is_json: + return request.json + elif request.form: + return dict(request.form) + else: + return request.data.decode('utf-8') + [email protected]("/test") +def test_param_query(): + return json.dumps(request.args) # This should return a JSON Review Comment: `json.dumps(request.args)` will raise `TypeError` because `request.args` is an (Immutable)MultiDict and not JSON serializable. Convert it first (e.g., `request.args.to_dict(flat=False)` / `.to_dict()`) and return a proper JSON response/content-type. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + + # Parse status code from status string (e.g., "200 OK" -> 200) + status_code = 200 + if response_status: + try: + status_code = int(response_status.split()[0]) + except (ValueError, IndexError): + status_code = 200 + + return { + "statusCode": status_code, + "headers": headers_dict, + "body": response_body.decode('utf-8') if isinstance(response_body, bytes) else response_body, + } + +while True: + line = stdin.readline() + if not line: break + args = json.loads(line) + + reset_environ() + res = {} + Review Comment: `response_status`/`response_headers` are global and never reset between requests. If an invocation errors before calling `start_response`, the next request may reuse the previous status/headers. Reset these (and ideally make them per-request locals) at the start of each loop iteration. ########## runtime/experimental/python/flask/Dockerfile: ########## @@ -0,0 +1,55 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +#ARG COMMON=missing:missing +ARG COMMON=registry.hub.docker.com/apache/openserverless-runtime-common:common1.18.4 Review Comment: This Dockerfile hardcodes a default `COMMON` image (`registry.hub.docker.com/apache/openserverless-runtime-common:common1.18.4`). Other runtimes in this repo use `ARG COMMON=missing:missing` to force the build pipeline to inject the intended proxy image (e.g., `runtime/python/v3.12/Dockerfile:18` and `runtime/experimental/python/v3.11cuda/Dockerfile:19`). Aligning with that pattern avoids accidental builds against an unintended registry/tag. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + + # Parse status code from status string (e.g., "200 OK" -> 200) + status_code = 200 + if response_status: + try: + status_code = int(response_status.split()[0]) + except (ValueError, IndexError): + status_code = 200 + + return { + "statusCode": status_code, + "headers": headers_dict, + "body": response_body.decode('utf-8') if isinstance(response_body, bytes) else response_body, + } + +while True: + line = stdin.readline() + if not line: break + args = json.loads(line) + + reset_environ() + res = {} Review Comment: Unlike the standard python launcher, this code does not propagate non-`value` fields from the activation payload into `__OW_*` environment variables (e.g., `activation_id`, `deadline`, `namespace`). Some user code and tooling rely on these being present. Consider mirroring `runtime/python/v3.12/lib/launcher.py` behavior for compatibility. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests Review Comment: `requests` and `base64` are imported but never used in this launcher. Please remove unused imports to keep the launcher minimal and avoid implying unsupported behavior. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, Review Comment: `wsgi.run_once` is set to `True`, but this launcher is a long-running loop handling multiple requests. Per WSGI semantics this should be `False` (and `wsgi.multiprocess` should reflect the actual execution model). Incorrect values can affect middleware/framework behavior. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + Review Comment: Converting WSGI response headers to a dict drops duplicate headers (notably multiple `Set-Cookie` headers). This can break apps that set multiple cookies. Consider preserving headers as a list of tuples, or aggregating only where safe while keeping `Set-Cookie` as a list. ########## packages/python/flask/app.py: ########## @@ -0,0 +1,43 @@ +#--web true +#--docker https://registry.hub.docker.com/94lama/python:flask + +import json +from flask import Flask, request +from hello import hello Review Comment: `from hello import hello` will fail at import time because this package directory contains only `app.py` (no `hello.py`). Remove the import or add the missing module; as-is the example action cannot start. ########## packages/python/flask/app.py: ########## @@ -0,0 +1,43 @@ +#--web true +#--docker https://registry.hub.docker.com/94lama/python:flask + Review Comment: The action annotation points to a personal Docker image (`94lama/python:flask`) rather than the runtime image produced by this repository. This makes the example non-reproducible for users; please reference the intended OpenServerless Flask runtime image/tag (or remove the annotation if it’s not meant to be used). ########## runtime/experimental/python/flask/requirements.txt: ########## @@ -0,0 +1,2 @@ +requests==2.32.3 Review Comment: `requests` is pinned in this runtime but the launcher code doesn’t use it. If it’s not intended to be a baked-in dependency for Flask actions, consider removing it to reduce image size and supply-chain surface area. ########## runtime/experimental/python/flask/lib/launcher.py: ########## @@ -0,0 +1,244 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from __future__ import print_function +from sys import stdin, stdout, stderr +from os import fdopen +from io import BytesIO +import base64 +import urllib.parse +import sys, os, json, traceback, requests + + +try: + # if the directory 'virtualenv' is extracted out of a zip file + path_to_virtualenv = os.path.abspath('./virtualenv') + if os.path.isdir(path_to_virtualenv): + # activate the virtualenv using activate_this.py contained in the virtualenv + activate_this_file = path_to_virtualenv + '/bin/activate_this.py' + if not os.path.exists(activate_this_file): # try windows path + activate_this_file = path_to_virtualenv + '/Scripts/activate_this.py' + if os.path.exists(activate_this_file): + with open(activate_this_file) as f: + code = compile(f.read(), activate_this_file, 'exec') + exec(code, dict(__file__=activate_this_file)) + else: + stderr.write("Invalid virtualenv. Zip file does not include 'activate_this.py'.\n") + sys.exit(1) +except Exception: + traceback.print_exc(file=sys.stderr, limit=0) + sys.exit(1) + +# now import the action as process input/output +from main__ import app as app + +out = fdopen(3, "wb") +if os.getenv("__OW_WAIT_FOR_ACK", "") != "": + out.write(json.dumps({"ok": True}, ensure_ascii=False).encode('utf-8')) + out.write(b'\n') + out.flush() + +env = os.environ + +global response_status +global response_headers +response_status = None +response_headers = None + +def start_response(status, headers): + global response_status + global response_headers + response_status = status + response_headers = headers + return [] + +environ = { + "wsgi.version": (1, 0), + "wsgi.url_scheme": "HTTP", + "wsgi.input": stdin, + "wsgi.output": stdout, + "wsgi.errors": stderr, + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": True, + "ACCEPT": "*/*", + "PATH_INFO": "/", + "CONTENT_TYPE": "application/json", + "CONTENT_LENGTH": "0", + "QUERY_STRING": "", + "REQUEST_METHOD": "GET", + "SERVER_PROTOCOL": "HTTP/1.1", +} + +def reset_environ(): + environ["wsgi.input"]=stdin + environ["ACCEPT"]="*/*" + environ["CONTENT_TYPE"]="application/json" + environ["CONTENT_LENGTH"] = "0" + environ["PATH_INFO"] = "/" + environ["QUERY_STRING"] = "" + environ["REQUEST_METHOD"]="GET" + +# Collect the response body +def build_response(): + response_body = b'' + response = app(environ, start_response) + try: + for chunk in response: + if isinstance(chunk, str): + chunk = chunk.encode('utf-8') + response_body += chunk + except Exception as e: + stderr.write(f"Error building response: {e}\n") + response_body = f'Error building response: {e}'.encode('utf-8') + + # Convert WSGI headers list to a dictionary + headers_dict = {} + if response_headers: + for header_name, header_value in response_headers: + headers_dict[header_name] = header_value + + # Parse status code from status string (e.g., "200 OK" -> 200) + status_code = 200 + if response_status: + try: + status_code = int(response_status.split()[0]) + except (ValueError, IndexError): + status_code = 200 + + return { + "statusCode": status_code, + "headers": headers_dict, + "body": response_body.decode('utf-8') if isinstance(response_body, bytes) else response_body, Review Comment: The response body is always decoded as UTF-8 when it’s bytes. If a Flask app returns non-UTF8/binary content, this will raise `UnicodeDecodeError` and fail the invocation. Consider returning the body as base64 with an `isBase64Encoded` flag (or only decode when content-type is textual). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
