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]


Reply via email to