# Copyright (c) 2021, 2024 Wind River Systems, Inc
#
# Licensed 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.
import json
from xml import etree as et
from oslo_log import log
import webob
from dcorch.api.proxy.common.service import Middleware
LOG = log.getLogger(__name__)
# As per webob.exc code:
# https://github.com/Pylons/webob/blob/master/src/webob/exc.py
# The explanation field is added to the HTTP exception as following:
# ${explanation}
WEBOB_EXPL_SEP = "
"
SOFTWARE_RELEASE_PATH = "/v1/release"
class ParseError(Middleware):
"""WSGI middleware to replace the plain text message body of an
error response with one formatted so the client can parse it.
Based on pecan.middleware.errordocument
"""
def __init__(self, app, conf):
self.app = app
def __call__(self, environ, start_response):
# Request for this state, modified by replace_start_response()
# and used when an error is being reported.
state = {}
def replacement_start_response(status, headers, exc_info=None):
"""Overrides the default response to make errors parsable."""
try:
status_code = int(status.split(" ")[0])
state["status_code"] = status_code
except (ValueError, TypeError): # pragma: nocover
raise Exception(
("ErrorDocumentMiddleware received an invalid status %s" % status)
)
else:
if (state["status_code"] // 100) not in (2, 3):
# Remove some headers so we can replace them later
# when we have the full error message and can
# compute the length.
headers = [
(h, v)
for (h, v) in headers
if h not in ("Content-Length", "Content-Type")
]
# Save the headers in case we need to modify them.
state["headers"] = headers
return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response)
req = webob.Request(environ)
# NOTE: The SOFTWARE_RELEASE_PATH is excluded because the software client
# does not expect the same error_message format like cgts client does
if (state["status_code"] // 100) not in (
2,
3,
) and SOFTWARE_RELEASE_PATH not in req.path:
if (
req.accept.best_match(["application/json", "application/xml"])
== "application/xml"
):
try:
# simple check xml is valid
body = [
et.ElementTree.tostring(
et.ElementTree.fromstring(
""
+ "\n".join(app_iter)
+ ""
)
)
]
except et.ElementTree.ParseError as err:
LOG.error("Error parsing HTTP response: %s" % err)
body = [
"%s" % state["status_code"] + ""
]
state["headers"].append(("Content-Type", "application/xml"))
else:
app_iter = [i.decode("utf-8") for i in app_iter]
# Parse explanation field from webob.exc and add it as
# 'faulstring' to be processed by cgts-client
fault = None
app_data = "\n".join(app_iter)
for data in app_data.split("\n"):
if WEBOB_EXPL_SEP in str(data):
# Remove separator, trailing and leading white spaces
fault = str(data).replace(WEBOB_EXPL_SEP, "").strip()
break
if fault is None:
body = [json.dumps({"error_message": app_data})]
else:
body = [
json.dumps(
{"error_message": json.dumps({"faultstring": fault})}
)
]
body = [item.encode("utf-8") for item in body]
state["headers"].append(("Content-Type", "application/json"))
state["headers"].append(("Content-Length", str(len(body[0]))))
else:
body = app_iter
return body