Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save michele-tn/74af1983d21efe66507498d33aa83f35 to your computer and use it in GitHub Desktop.

Select an option

Save michele-tn/74af1983d21efe66507498d33aa83f35 to your computer and use it in GitHub Desktop.
Flask Reverse Proxy — Complete Production-Ready Guide

Flask Reverse Proxy — Complete Production-Ready Guide

Based On

This guide is based on the following sources:

It expands the original concepts into a modern, production-oriented implementation.


Table of Contents

  1. Introduction
  2. Reverse Proxy Fundamentals
  3. Reverse Proxy vs Forward Proxy
  4. Architecture Overview
  5. Basic Flask Reverse Proxy
  6. Full HTTP Method Support
  7. Query String Forwarding
  8. Header Forwarding
  9. Timeout Handling
  10. Error Handling
  11. Logging
  12. Streaming Responses
  13. Security Best Practices
  14. HTTPS / TLS
  15. Production Deployment
  16. Docker Deployment
  17. Gunicorn Deployment
  18. Nginx Integration
  19. WebSocket Limitations
  20. Performance Considerations
  21. Async Alternatives
  22. Complete Production-Ready Example
  23. Testing Examples
  24. Troubleshooting
  25. Conclusion

1. Introduction

A reverse proxy is a server that sits between clients and backend services.

Instead of clients communicating directly with the upstream application, requests first reach the proxy server, which then forwards the requests to the target service.

Common use cases include:

  • Load balancing
  • Security filtering
  • Authentication
  • Request logging
  • API gateways
  • Caching
  • SSL termination
  • Microservices routing

Flask can be used to create lightweight reverse proxy applications for development, testing, internal tools, and lightweight API routing.


2. Reverse Proxy Fundamentals

A proxy server acts as an intermediary between two systems.

Depending on where the proxy is positioned, it can behave as:

  • a Forward Proxy
  • a Reverse Proxy

Understanding the difference is fundamental when designing network architectures, API gateways, or security layers.


3. Reverse Proxy vs Forward Proxy

Forward Proxy

A forward proxy sits in front of the client.

The client sends requests to the proxy, and the proxy forwards them to external servers.

Typical use cases:

  • privacy
  • anonymity
  • bypassing geographic restrictions
  • enterprise internet filtering
  • caching external resources

Flow

Client → Forward Proxy → Internet → Target Server

Typical Examples

  • Corporate proxies
  • VPN services
  • Anonymous browsing systems

Reverse Proxy

A reverse proxy sits in front of backend servers.

Clients communicate with the reverse proxy instead of directly communicating with application servers.

Typical use cases:

  • load balancing
  • TLS termination
  • API routing
  • authentication
  • DDoS protection
  • caching
  • traffic inspection

Flow

Client → Reverse Proxy → Backend Servers

Visual Comparison — Reverse vs Forward Proxy Diagram

Reverse Proxy Proxy


Forward Proxy


Key Differences

Reverse Proxy Forward Proxy
Protects servers Protects clients
Server-side Client-side
Used by websites and APIs Used by users or organizations
Hides backend infrastructure Hides client identity
Common in cloud architectures Common in enterprise networks

Why Reverse Proxies Are Important

Modern infrastructures rely heavily on reverse proxies because they:

  • centralize traffic management
  • improve security
  • simplify scaling
  • reduce backend exposure
  • enable observability and monitoring
  • support zero-trust architectures

Popular reverse proxy technologies include:

  • Nginx
  • HAProxy
  • Envoy
  • Traefik
  • Cloudflare Edge
  • API Gateways

Flask-based reverse proxies are typically used for:

  • custom routing logic
  • API middleware
  • authentication layers
  • development environments
  • lightweight internal tooling

4. Architecture Overview

┌─────────────┐
│   Client    │
└──────┬──────┘
       │ HTTP Request
       ▼
┌────────────────────┐
│ Flask ReverseProxy │
│     proxy.py       │
└─────────┬──────────┘
          │ Forward Request
          ▼
┌────────────────────┐
│  Upstream Service  │
│ localhost:8000/api │
└────────────────────┘

5. Basic Flask Reverse Proxy

Minimal Example

from flask import Flask, request, Response
import requests

app = Flask(__name__)

UPSTREAM = "http://localhost:8000"

@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def proxy(path):
    url = f"{UPSTREAM}/{path}"

    response = requests.get(url)

    return Response(
        response.content,
        status=response.status_code,
        content_type=response.headers.get("content-type")
    )

if __name__ == "__main__":
    app.run(port=1337)

6. Full HTTP Method Support

Production proxies should support all major HTTP methods.

methods = [
    "GET",
    "POST",
    "PUT",
    "PATCH",
    "DELETE",
    "OPTIONS",
    "HEAD"
]

7. Query String Forwarding

response = requests.request(
    method=request.method,
    url=url,
    params=request.args
)

8. Header Forwarding

headers = {
    key: value
    for key, value in request.headers
    if key.lower() != "host"
}

9. Timeout Handling

timeout=(5, 30)

10. Error Handling

try:
    response = requests.request(...)
except requests.exceptions.RequestException as e:
    return {
        "error": str(e)
    }, 502

11. Logging

import logging

logging.basicConfig(level=logging.INFO)

12. Streaming Responses

stream=True

13. Security Best Practices

Avoid Open Proxy Vulnerabilities

Never allow arbitrary upstream URLs without validation.

Dangerous example:

target = request.args.get("url")

14. HTTPS / TLS

Recommended production stack:

Client
   ↓
Nginx / Caddy
   ↓
Gunicorn
   ↓
Flask Proxy

15. Production Deployment

Recommended components:

  • Flask
  • Gunicorn
  • Nginx
  • Redis
  • Docker

16. Docker Deployment

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install -r requirements.txt

COPY . .

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:1337", "proxy:app"]

17. Gunicorn Deployment

gunicorn -w 4 -b 0.0.0.0:1337 proxy:app

18. Nginx Integration

server {
    listen 80;

    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:1337;
    }
}

19. WebSocket Limitations

Flask + requests does not support native WebSocket proxying.


20. Performance Considerations

Flask proxies are slower than:

  • Nginx
  • HAProxy
  • Envoy

because of:

  • Python overhead
  • synchronous I/O
  • WSGI limitations

21. Async Alternatives

Example:

async with httpx.AsyncClient() as client:

22. Complete Production-Ready Example

import os
import logging
import requests

from flask import Flask
from flask import request
from flask import Response

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)

UPSTREAM = os.getenv(
    "UPSTREAM_URL",
    "http://localhost:8000"
)

METHODS = [
    "GET",
    "POST",
    "PUT",
    "PATCH",
    "DELETE",
    "OPTIONS",
    "HEAD"
]

@app.route("/", defaults={"path": ""}, methods=METHODS)
@app.route("/<path:path>", methods=METHODS)
def proxy(path):

    url = f"{UPSTREAM}/{path}"

    headers = {
        key: value
        for key, value in request.headers
        if key.lower() != "host"
    }

    try:

        response = requests.request(
            method=request.method,
            url=url,
            headers=headers,
            params=request.args,
            data=request.get_data(),
            timeout=(5, 30)
        )

        return Response(
            response.content,
            status=response.status_code
        )

    except requests.exceptions.RequestException as e:

        return {
            "error": str(e)
        }, 502

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=1337)

23. Testing Examples

curl http://127.0.0.1:1337/api/users

24. Troubleshooting

502 Bad Gateway

Usually caused by:

  • upstream offline
  • timeout
  • invalid upstream URL

25. Conclusion

Flask reverse proxies are excellent for:

  • development
  • internal APIs
  • authentication middleware
  • lightweight routing

For high-performance production environments, prefer:

  • Nginx
  • HAProxy
  • Envoy
  • Traefik
@michele-tn

Copy link
Copy Markdown
Author

S/4HANA Cloud Local Reverse Proxy — Flask HTTPS Testing Guide

Overview

This guide documents a local Flask reverse proxy named MinimalProxy.py, designed for authorized testing and debugging of SAP S/4HANA Cloud-style flows in a local HTTPS environment.

The script keeps the same route naming convention used in the working example:

UPSTREAMS = {
    "s4": "https://tenant-example.s4hana.cloud.sap/",
    "idp": "https://tenant-example.accounts.cloud.sap/",
    "idp_od": "https://tenant-example.accounts.ondemand.com/",
}

All tenant names, hostnames, and identifiers have been anonymized.


Use cases

  • Local HTTPS reverse proxy testing
  • SAP S/4HANA Cloud integration debugging
  • Redirect and header inspection
  • CORS behavior analysis
  • SAML/OIDC-like flow troubleshooting in authorized environments
  • Development sandbox experiments

Security and privacy notes

Do not publish real values such as:

  • real tenant identifiers
  • company-specific domains
  • authentication cookies
  • SAML assertions
  • OAuth/OIDC tokens
  • internal URLs
  • request/response payloads containing sensitive data

This example uses placeholder domains such as:

tenant-example.s4hana.cloud.sap
tenant-example.accounts.cloud.sap
tenant-example.accounts.ondemand.com

Complete script: MinimalProxy.py

# MinimalProxy.py

import logging
from urllib.parse import urljoin, urlparse

import requests
from flask import Flask, request, Response, redirect

app = Flask(__name__)

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

LOCAL_ORIGIN = "https://127.0.0.1:1337"

UPSTREAMS = {
    "s4": "https://tenant-example.s4hana.cloud.sap/",
    "idp": "https://tenant-example.accounts.cloud.sap/",
    "idp_od": "https://tenant-example.accounts.ondemand.com/",
}

METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]


def upstream_host(upstream):
    return urlparse(upstream).netloc


def local_prefix(name):
    return f"{LOCAL_ORIGIN}/{name}"


def rewrite_absolute_url(value):
    if not value:
        return value

    rewritten = value

    for name, upstream in UPSTREAMS.items():
        host = upstream_host(upstream)
        base = upstream.rstrip("/")

        rewritten = rewritten.replace(base, local_prefix(name))
        rewritten = rewritten.replace(f"https://{host}", local_prefix(name))
        rewritten = rewritten.replace(f"http://{host}", local_prefix(name))
        rewritten = rewritten.replace(
            f"https:\\/\\/{host}",
            local_prefix(name).replace("https://", "https:\\/\\/")
        )
        rewritten = rewritten.replace(
            f"http:\\/\\/{host}",
            local_prefix(name).replace("https://", "https:\\/\\/")
        )

    return rewritten


def rewrite_location(value, current_name):
    if not value:
        return value

    value = rewrite_absolute_url(value)

    if value.startswith("/"):
        return local_prefix(current_name) + value

    return value


def normalize_path_for_upstream(name, path):
    if name == "idp":
        path = path.replace(
            "127.0.0.1:1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )
        path = path.replace(
            "127.0.0.1%3A1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )
        path = path.replace(
            "127.0.0.1%3a1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )

    return path


def clean_request_headers(upstream):
    headers = {
        "Host": upstream_host(upstream),
        "Connection": "close",
        "Accept-Encoding": "identity",
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        ),
        "Accept": request.headers.get("Accept", "*/*"),
        "Accept-Language": request.headers.get(
            "Accept-Language",
            "it-IT,it;q=0.9,en;q=0.8"
        ),
    }

    if request.content_type:
        headers["Content-Type"] = request.content_type

    return headers


def clean_response_headers(resp, current_name):
    headers = []

    blocked = {
        "connection",
        "keep-alive",
        "proxy-authenticate",
        "proxy-authorization",
        "te",
        "trailer",
        "transfer-encoding",
        "upgrade",
        "content-length",
        "content-encoding",
        "content-security-policy",
        "content-security-policy-report-only",
        "x-frame-options",
        "strict-transport-security",
    }

    for key, value in resp.headers.items():
        lk = key.lower()

        if lk in blocked:
            continue

        if lk == "location":
            value = rewrite_location(value, current_name)

        if lk == "set-cookie":
            for upstream in UPSTREAMS.values():
                host = upstream_host(upstream)
                value = value.replace(f"Domain={host};", "")
                value = value.replace(f"Domain=.{host};", "")
                value = value.replace(f"domain={host};", "")
                value = value.replace(f"domain=.{host};", "")

        headers.append((key, value))

    origin = request.headers.get("Origin", LOCAL_ORIGIN)

    headers.append(("Access-Control-Allow-Origin", origin))
    headers.append(("Access-Control-Allow-Credentials", "true"))
    headers.append(("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"))
    headers.append(("Access-Control-Allow-Headers", request.headers.get(
        "Access-Control-Request-Headers",
        "Authorization, Content-Type, X-CSRF-Token, X-Requested-With, Accept, Origin"
    )))
    headers.append(("Access-Control-Expose-Headers", "Location, Set-Cookie, X-CSRF-Token"))
    headers.append(("Vary", "Origin"))

    return headers


def rewrite_body(content, content_type, encoding):
    if not content:
        return content

    ct = content_type.lower()

    rewrite_types = [
        "text/html",
        "application/javascript",
        "text/javascript",
        "application/json",
        "text/css",
        "application/xml",
        "text/xml",
    ]

    if not any(t in ct for t in rewrite_types):
        return content

    try:
        text = content.decode(encoding or "utf-8", errors="ignore")
        text = rewrite_absolute_url(text)

        text = text.replace(
            "tenant-example.accounts.cloud.sap/saml2/idp/sso/tenant-example.accounts.ondemand.com",
            "127.0.0.1:1337/idp/saml2/idp/sso/tenant-example.accounts.ondemand.com"
        )

        text = text.replace(
            "https://tenant-example.accounts.cloud.sap",
            "https://127.0.0.1:1337/idp"
        )

        text = text.replace(
            "https://tenant-example.accounts.ondemand.com",
            "https://127.0.0.1:1337/idp_od"
        )

        text = text.replace(
            "https:\\/\\/tenant-example.accounts.cloud.sap",
            "https:\\/\\/127.0.0.1:1337\\/idp"
        )

        text = text.replace(
            "https:\\/\\/tenant-example.accounts.ondemand.com",
            "https:\\/\\/127.0.0.1:1337\\/idp_od"
        )

        return text.encode("utf-8")

    except Exception as exc:
        logging.warning("Body rewrite failed: %s", exc)
        return content


@app.route("/")
def index():
    return redirect("/s4/", code=302)


@app.route("/favicon.ico")
def favicon():
    return Response("", status=204)


@app.route("/universalui/<path:path>", methods=METHODS)
def universalui_assets(path):
    return proxy("idp", f"universalui/{path}")


@app.route("/ui/<path:path>", methods=METHODS)
def idp_ui_assets(path):
    return proxy("idp", f"ui/{path}")


@app.route("/<name>/", defaults={"path": ""}, methods=METHODS)
@app.route("/<name>/<path:path>", methods=METHODS)
def proxy(name, path):
    upstream = UPSTREAMS.get(name)

    if not upstream:
        return Response(
            f"Unknown upstream route: {name}",
            status=404,
            content_type="text/plain"
        )

    if request.method == "OPTIONS":
        return Response(
            "",
            status=204,
            headers={
                "Access-Control-Allow-Origin": request.headers.get("Origin", LOCAL_ORIGIN),
                "Access-Control-Allow-Credentials": "true",
                "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
                "Access-Control-Allow-Headers": request.headers.get(
                    "Access-Control-Request-Headers",
                    "Authorization, Content-Type, X-CSRF-Token, X-Requested-With, Accept, Origin"
                ),
                "Access-Control-Max-Age": "86400",
            },
        )

    path = normalize_path_for_upstream(name, path)
    target = urljoin(upstream, path)
    body = request.get_data()

    logging.info(
        "%s /%s/%s?%s -> %s",
        request.method,
        name,
        path,
        request.query_string.decode(errors="ignore"),
        target
    )

    logging.info(
        "REQUEST BODY length=%s content_type=%s",
        len(body),
        request.content_type
    )

    try:
        session = requests.Session()
        session.trust_env = True

        resp = session.request(
            method=request.method,
            url=target,
            params=request.args,
            headers=clean_request_headers(upstream),
            data=body if request.method not in ["GET", "HEAD"] else None,
            allow_redirects=False,
            timeout=30,
            verify=True,
        )

        logging.info(
            "UPSTREAM %s %s",
            resp.status_code,
            resp.headers.get("Location", "")
        )

        content_type = resp.headers.get("Content-Type", "")
        content = rewrite_body(resp.content, content_type, resp.encoding)

        return Response(
            content,
            status=resp.status_code,
            headers=clean_response_headers(resp, name),
        )

    except requests.exceptions.SSLError as exc:
        logging.exception("SSL error")
        return Response(
            f"SSL error verso upstream:\n{exc}",
            status=502,
            content_type="text/plain",
        )

    except requests.exceptions.ConnectionError as exc:
        logging.exception("Connection error")
        return Response(
            f"Connection reset/connection error verso upstream:\n{exc}",
            status=502,
            content_type="text/plain",
        )

    except requests.exceptions.RequestException as exc:
        logging.exception("Proxy error")
        return Response(
            f"Proxy error verso upstream:\n{exc}",
            status=502,
            content_type="text/plain",
        )


if __name__ == "__main__":
    app.run(
        host="127.0.0.1",
        port=1337,
        debug=True,
        threaded=True,
        ssl_context="adhoc",
    )

Installation

Create a virtual environment:

python -m venv .venv

Activate it.

Windows PowerShell:

.\.venv\Scripts\Activate.ps1

Linux/macOS:

source .venv/bin/activate

Install dependencies:

pip install flask requests pyopenssl

Run

From the folder containing MinimalProxy.py:

python MinimalProxy.py

The proxy listens on:

https://127.0.0.1:1337

Browser access

Open:

https://127.0.0.1:1337/

The browser may show a certificate warning because the script uses:

ssl_context="adhoc"

This creates a temporary self-signed certificate for local testing.


Routes

Local route Upstream placeholder
/s4/ https://tenant-example.s4hana.cloud.sap/
/idp/ https://tenant-example.accounts.cloud.sap/
/idp_od/ https://tenant-example.accounts.ondemand.com/
/universalui/... proxied to idp
/ui/... proxied to idp

Quick test with curl

curl -k -I https://127.0.0.1:1337/

Expected behavior: a local response or redirect from the proxy.


Browser console test

Open DevTools on the local proxy page and run:

fetch("https://127.0.0.1:1337/s4/", {
  credentials: "include"
})
.then(r => console.log("STATUS:", r.status, "URL:", r.url))
.catch(console.error);

Troubleshooting

ERR_CONNECTION_RESET

Usually indicates that the upstream reset the connection, or that an authentication flow does not tolerate path-based reverse proxying.

Check the Flask logs for lines like:

Connection reset/connection error verso upstream

Blank page after authentication redirect

Check browser DevTools → Network. Missing CSS/JS assets often indicate that asset paths need route mapping, such as:

@app.route("/universalui/<path:path>", methods=METHODS)
def universalui_assets(path):
    return proxy("idp", f"universalui/{path}")

CORS error

CORS is enforced by the browser, not by curl or Postman. Test CORS from a browser page, not from command-line tools.


Production note

This Flask proxy is intended for local testing only.

For production-grade reverse proxying, use:

  • Nginx
  • HAProxy
  • Envoy
  • Traefik
  • a managed API Gateway

Disclaimer

Use only in environments where you have explicit authorization. This guide is for local development, debugging, and interoperability testing.

@michele-tn

Copy link
Copy Markdown
Author

S/4HANA Cloud Local Reverse Proxy — Flask HTTPS Testing Guide

Overview

This guide documents a local Flask reverse proxy named MinimalProxy2_FIX_RESET.py, designed for authorized testing and debugging of SAP S/4HANA Cloud-style flows in a local HTTPS environment.

The script preserves the same route naming convention used in the working example below:

UPSTREAMS = {
    "s4": "https://tenant-example.s4hana.cloud.sap/",
    "idp": "https://tenant-example.accounts.cloud.sap/",
    "idp_od": "https://tenant-example.accounts.ondemand.com/",
}

All tenant names, hostnames, domains, and identifiers shown in this guide have been fully anonymized and are provided exclusively as placeholders for documentation purposes.


Main Improvements

The updated proxy introduces:

  • automatic retry on upstream TCP reset conditions;
  • improved resiliency for SAML/OIDC authentication flows;
  • better forwarding of browser headers;
  • proper handling of Cookie, Origin, and Referer;
  • configurable upstream timeout values;
  • controlled requests.Session() lifecycle;
  • improved logging and troubleshooting support.

Root Cause of the Original Issue

The original implementation occasionally failed during SAML POST authentication flows with errors such as:

Connection reset/connection error verso upstream:
('Connection aborted.', ConnectionResetError(10054, ...))

The upstream occasionally closed the TCP connection during SSL/SAML negotiation before returning a valid HTTP response.

The original proxy immediately returned HTTP 502 without retrying the request.


Retry Mechanism

The new implementation introduces:

def upstream_request_with_retry(...)

The retry logic:

  • retries only low-level TCP reset conditions;
  • preserves the original request unchanged;
  • retries only a limited number of times;
  • avoids changing the functional behavior of the proxy.

Configuration:

UPSTREAM_CONNECT_TIMEOUT = 10
UPSTREAM_READ_TIMEOUT = 75
UPSTREAM_RESET_RETRIES = 2
UPSTREAM_RETRY_SLEEP_SECONDS = 0.7

Complete Script — MinimalProxy2_FIX_RESET.py

# MinimalProxy.py

import logging
import time
from urllib.parse import urljoin, urlparse

import requests
from flask import Flask, request, Response, redirect

app = Flask(__name__)

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

LOCAL_ORIGIN = "https://127.0.0.1:1337"

UPSTREAMS = {
    "s4": "https://tenant-example.s4hana.cloud.sap/",
    "idp": "https://tenant-example.accounts.cloud.sap/",
    "idp_od": "https://tenant-example.accounts.ondemand.com/",
}

METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]

# Retry mirato per reset TCP sporadici lato upstream, tipici di handshake/SAML/proxy aziendali.
# Non cambia il comportamento funzionale del proxy: ripete solo la stessa richiesta quando
# la connessione viene chiusa prima di ricevere una risposta HTTP valida.
UPSTREAM_CONNECT_TIMEOUT = 10
UPSTREAM_READ_TIMEOUT = 75
UPSTREAM_RESET_RETRIES = 2
UPSTREAM_RETRY_SLEEP_SECONDS = 0.7


# Header hop-by-hop: non devono essere inoltrati a monte.
REQUEST_HOP_BY_HOP_HEADERS = {
    "host",
    "connection",
    "keep-alive",
    "proxy-authenticate",
    "proxy-authorization",
    "te",
    "trailer",
    "transfer-encoding",
    "upgrade",
    "content-length",
    "content-encoding",
}


def upstream_host(upstream):
    return urlparse(upstream).netloc


def upstream_origin(upstream):
    parsed = urlparse(upstream)
    return f"{parsed.scheme}://{parsed.netloc}"


def local_prefix(name):
    return f"{LOCAL_ORIGIN}/{name}"


def rewrite_absolute_url(value):
    if not value:
        return value

    rewritten = value

    for name, upstream in UPSTREAMS.items():
        host = upstream_host(upstream)
        base = upstream.rstrip("/")

        rewritten = rewritten.replace(base, local_prefix(name))
        rewritten = rewritten.replace(f"https://{host}", local_prefix(name))
        rewritten = rewritten.replace(f"http://{host}", local_prefix(name))
        rewritten = rewritten.replace(
            f"https:\\/\\/{host}",
            local_prefix(name).replace("https://", "https:\\/\\/")
        )
        rewritten = rewritten.replace(
            f"http:\\/\\/{host}",
            local_prefix(name).replace("https://", "https:\\/\\/")
        )

    return rewritten


def rewrite_local_url_to_upstream(value, current_name):
    """Riscrive Origin/Referer/Cookie-local URL dal proxy locale verso l'host reale upstream."""
    if not value:
        return value

    rewritten = value

    for name, upstream in UPSTREAMS.items():
        prefix = local_prefix(name)
        upstream_base = upstream.rstrip("/")

        rewritten = rewritten.replace(prefix, upstream_base)
        rewritten = rewritten.replace(prefix.replace("https://", "https:\\/\\/"), upstream_base.replace("https://", "https:\\/\\/"))

    # Se resta solo l'origine locale, usa l'origine dell'upstream corrente.
    if rewritten == LOCAL_ORIGIN:
        rewritten = upstream_origin(UPSTREAMS[current_name])

    return rewritten


def rewrite_location(value, current_name):
    if not value:
        return value

    value = rewrite_absolute_url(value)

    if value.startswith("/"):
        return local_prefix(current_name) + value

    return value


def normalize_path_for_upstream(name, path):
    if name == "idp":
        path = path.replace(
            "127.0.0.1:1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )
        path = path.replace(
            "127.0.0.1%3A1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )
        path = path.replace(
            "127.0.0.1%3a1337/idp_od",
            "tenant-example.accounts.ondemand.com"
        )

    return path


def clean_request_headers(upstream, current_name):
    headers = {}

    # Mantiene gli header utili inviati dal browser senza inoltrare quelli hop-by-hop.
    for key, value in request.headers.items():
        lk = key.lower()
        if lk in REQUEST_HOP_BY_HOP_HEADERS:
            continue
        if lk in {"origin", "referer"}:
            value = rewrite_local_url_to_upstream(value, current_name)
        headers[key] = value

    # Valori imposti/normalizzati dal proxy.
    headers["Host"] = upstream_host(upstream)
    headers["Connection"] = "close"
    headers["Accept-Encoding"] = "identity"

    if "User-Agent" not in headers:
        headers["User-Agent"] = (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/124.0.0.0 Safari/537.36"
        )

    if "Accept" not in headers:
        headers["Accept"] = "*/*"

    if "Accept-Language" not in headers:
        headers["Accept-Language"] = "it-IT,it;q=0.9,en;q=0.8"

    if request.content_type:
        headers["Content-Type"] = request.content_type

    return headers


def clean_response_headers(resp, current_name):
    headers = []

    blocked = {
        "connection",
        "keep-alive",
        "proxy-authenticate",
        "proxy-authorization",
        "te",
        "trailer",
        "transfer-encoding",
        "upgrade",
        "content-length",
        "content-encoding",
        "content-security-policy",
        "content-security-policy-report-only",
        "x-frame-options",
        "strict-transport-security",
    }

    for key, value in resp.headers.items():
        lk = key.lower()

        if lk in blocked:
            continue

        if lk == "location":
            value = rewrite_location(value, current_name)

        if lk == "set-cookie":
            for upstream in UPSTREAMS.values():
                host = upstream_host(upstream)
                value = value.replace(f"Domain={host};", "")
                value = value.replace(f"Domain=.{host};", "")
                value = value.replace(f"domain={host};", "")
                value = value.replace(f"domain=.{host};", "")

            # Il proxy locale è HTTPS su 127.0.0.1: Secure va bene; SameSite=None va bene.
            # Non forziamo altre modifiche per non alterare il flusso SAML.

        headers.append((key, value))

    origin = request.headers.get("Origin", LOCAL_ORIGIN)

    headers.append(("Access-Control-Allow-Origin", origin))
    headers.append(("Access-Control-Allow-Credentials", "true"))
    headers.append(("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"))
    headers.append(("Access-Control-Allow-Headers", request.headers.get(
        "Access-Control-Request-Headers",
        "Authorization, Content-Type, X-CSRF-Token, X-Requested-With, Accept, Origin"
    )))
    headers.append(("Access-Control-Expose-Headers", "Location, Set-Cookie, X-CSRF-Token"))
    headers.append(("Vary", "Origin"))

    return headers


def rewrite_body(content, content_type, encoding):
    if not content:
        return content

    ct = content_type.lower()

    rewrite_types = [
        "text/html",
        "application/javascript",
        "text/javascript",
        "application/json",
        "text/css",
        "application/xml",
        "text/xml",
    ]

    if not any(t in ct for t in rewrite_types):
        return content

    try:
        text = content.decode(encoding or "utf-8", errors="ignore")
        text = rewrite_absolute_url(text)

        text = text.replace(
            "tenant-example.accounts.cloud.sap/saml2/idp/sso/tenant-example.accounts.ondemand.com",
            "127.0.0.1:1337/idp/saml2/idp/sso/tenant-example.accounts.ondemand.com"
        )

        text = text.replace(
            "https://tenant-example.accounts.cloud.sap",
            "https://127.0.0.1:1337/idp"
        )

        text = text.replace(
            "https://tenant-example.accounts.ondemand.com",
            "https://127.0.0.1:1337/idp_od"
        )

        text = text.replace(
            "https:\\/\\/tenant-example.accounts.cloud.sap",
            "https:\\/\\/127.0.0.1:1337\\/idp"
        )

        text = text.replace(
            "https:\\/\\/tenant-example.accounts.ondemand.com",
            "https:\\/\\/127.0.0.1:1337\\/idp_od"
        )

        return text.encode("utf-8")

    except Exception as exc:
        logging.warning("Body rewrite failed: %s", exc)
        return content


def is_connection_reset(exc):
    text = repr(exc)
    return (
        "10054" in text
        or "ConnectionResetError" in text
        or "Connection aborted" in text
        or "RemoteDisconnected" in text
        or "Connection reset" in text
    )


def upstream_request_with_retry(method, target, upstream, current_name, body):
    last_exc = None

    for attempt in range(1, UPSTREAM_RESET_RETRIES + 2):
        session = requests.Session()
        session.trust_env = True

        try:
            logging.info(
                "UPSTREAM REQUEST attempt=%s/%s method=%s target=%s",
                attempt,
                UPSTREAM_RESET_RETRIES + 1,
                method,
                target,
            )

            return session.request(
                method=method,
                url=target,
                params=request.args,
                headers=clean_request_headers(upstream, current_name),
                data=body if method not in ["GET", "HEAD"] else None,
                allow_redirects=False,
                timeout=(UPSTREAM_CONNECT_TIMEOUT, UPSTREAM_READ_TIMEOUT),
                verify=True,
            )

        except requests.exceptions.ConnectionError as exc:
            last_exc = exc

            # Retry solo per reset/interruzioni di connessione prima della risposta HTTP.
            if attempt <= UPSTREAM_RESET_RETRIES and is_connection_reset(exc):
                logging.warning(
                    "Connection reset verso upstream; retry %s/%s tra %.1fs: %s",
                    attempt,
                    UPSTREAM_RESET_RETRIES,
                    UPSTREAM_RETRY_SLEEP_SECONDS,
                    exc,
                )
                time.sleep(UPSTREAM_RETRY_SLEEP_SECONDS)
                continue

            raise

        finally:
            session.close()

    raise last_exc


@app.route("/")
def index():
    return redirect("/s4/", code=302)


@app.route("/favicon.ico")
def favicon():
    return Response("", status=204)


@app.route("/universalui/<path:path>", methods=METHODS)
def universalui_assets(path):
    return proxy("idp", f"universalui/{path}")


@app.route("/ui/<path:path>", methods=METHODS)
def idp_ui_assets(path):
    return proxy("idp", f"ui/{path}")


@app.route("/<name>/", defaults={"path": ""}, methods=METHODS)
@app.route("/<name>/<path:path>", methods=METHODS)
def proxy(name, path):
    upstream = UPSTREAMS.get(name)

    if not upstream:
        return Response(
            f"Unknown upstream route: {name}",
            status=404,
            content_type="text/plain"
        )

    if request.method == "OPTIONS":
        return Response(
            "",
            status=204,
            headers={
                "Access-Control-Allow-Origin": request.headers.get("Origin", LOCAL_ORIGIN),
                "Access-Control-Allow-Credentials": "true",
                "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
                "Access-Control-Allow-Headers": request.headers.get(
                    "Access-Control-Request-Headers",
                    "Authorization, Content-Type, X-CSRF-Token, X-Requested-With, Accept, Origin"
                ),
                "Access-Control-Max-Age": "86400",
            },
        )

    path = normalize_path_for_upstream(name, path)
    target = urljoin(upstream, path)
    body = request.get_data()

    logging.info(
        "%s /%s/%s?%s -> %s",
        request.method,
        name,
        path,
        request.query_string.decode(errors="ignore"),
        target
    )

    logging.info(
        "REQUEST BODY length=%s content_type=%s",
        len(body),
        request.content_type
    )

    try:
        resp = upstream_request_with_retry(
            method=request.method,
            target=target,
            upstream=upstream,
            current_name=name,
            body=body,
        )

        logging.info(
            "UPSTREAM %s %s",
            resp.status_code,
            resp.headers.get("Location", "")
        )

        content_type = resp.headers.get("Content-Type", "")
        content = rewrite_body(resp.content, content_type, resp.encoding)

        return Response(
            content,
            status=resp.status_code,
            headers=clean_response_headers(resp, name),
        )

    except requests.exceptions.SSLError as exc:
        logging.exception("SSL error")
        return Response(
            f"SSL error verso upstream:\n{exc}",
            status=502,
            content_type="text/plain",
        )

    except requests.exceptions.ConnectionError as exc:
        logging.exception("Connection error")
        return Response(
            f"Connection reset/connection error verso upstream dopo retry:\n{exc}",
            status=502,
            content_type="text/plain",
        )

    except requests.exceptions.RequestException as exc:
        logging.exception("Proxy error")
        return Response(
            f"Proxy error verso upstream:\n{exc}",
            status=502,
            content_type="text/plain",
        )


if __name__ == "__main__":
    app.run(
        host="127.0.0.1",
        port=1337,
        debug=True,
        threaded=True,
        ssl_context="adhoc",
    )

Installation

Create a virtual environment:

python -m venv .venv

Activate it.

Windows PowerShell

.\.venv\Scripts\Activate.ps1

Linux/macOS

source .venv/bin/activate

Install dependencies:

pip install flask requests pyopenssl

Run

From the folder containing MinimalProxy2_FIX_RESET.py:

python MinimalProxy2_FIX_RESET.py

The proxy listens on:

https://127.0.0.1:1337

Browser Access

Open:

https://127.0.0.1:1337/

The browser may show a certificate warning because the script uses:

ssl_context="adhoc"

This creates a temporary self-signed certificate for local testing.


Routes

Local Route Upstream Placeholder
/s4/ https://tenant-example.s4hana.cloud.sap/
/idp/ https://tenant-example.accounts.cloud.sap/
/idp_od/ https://tenant-example.accounts.ondemand.com/
/universalui/... proxied to idp
/ui/... proxied to idp

Troubleshooting

Connection Reset

Typical log:

Connection reset verso upstream; retry 1/2 tra 0.7s

The proxy retries automatically before returning HTTP 502.


Final Fallback Error

Only after all retries fail:

Connection reset/connection error verso upstream dopo retry

Blank Page After Authentication

Check browser DevTools → Network.

Missing CSS/JS assets often indicate missing route mappings.


CORS Errors

CORS is enforced by the browser, not by curl or Postman.

Always test CORS directly from a browser session.


Security Notes

Do NOT expose:

  • real tenant names;
  • authentication cookies;
  • SAML assertions;
  • OAuth tokens;
  • internal domains;
  • sensitive payloads.

Use only placeholder values in shared documentation.


Production Note

This Flask proxy is intended only for:

  • local debugging;
  • interoperability testing;
  • reverse proxy analysis.

For production-grade reverse proxying, use:

  • Nginx
  • HAProxy
  • Envoy
  • Traefik
  • managed API gateways.

Disclaimer

Use only in environments where you have explicit authorization.

This guide is intended exclusively for:

  • local development;
  • debugging;
  • interoperability testing;
  • authorized reverse proxy analysis.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment