Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unexpected SSLEOFError when Receiving Redirect (307) Response with Large Data Payload #6885

Open
Nahemah1022 opened this issue Feb 4, 2025 · 3 comments

Comments

@Nahemah1022
Copy link

Nahemah1022 commented Feb 4, 2025

This issue occurs only when TLS is enabled on both the client and server sides.

When a requests client sends a large PUT request and receives an HTTP 307 redirect, the server may close the connection early before the payload is fully transmitted. This happens because the server issuing the redirect does not need to process the request body, so it terminates the connection as soon as it sends the redirect response. However, requests does not handle this scenario gracefully and raises the following SSL error:

urllib3.exceptions.SSLError: EOF occurred in violation of protocol (_ssl.c:2426)

Expected Result

If an HTTP 307 redirect is received before the payload is fully transmitted, requests should handle the redirect properly without complaining on the previous early-closed connection. The client should not fail simply because the server closed the connection early.

Actual Result

requests fails with SSLEOFError instead of retrying the request to the redirected location.
This issue does not occur with small payloads (maybe because they are typically sent in a single transmission chunk and completes before the server closes it?)

Reproduction Steps

Client-Side Code

import requests
requests.put("https://localhost:5000/", data="A" * 10_000_000, verify="/path/to/openssl/certs/ca.crt")
...
>> urllib3.exceptions.SSLError: EOF occurred in violation of protocol (_ssl.c:2426)

Server-Side Code (Minimal Proxy)

import http.server
import socketserver
import ssl
import urllib.parse

CERT_FILE = "/path/to/openssl/certs/server.crt"
KEY_FILE = "/path/to/openssl/certs/server.key"
REDIRECT_BASE_URL = "https://google.com"

class ProxyHandler(http.server.BaseHTTPRequestHandler):
    def do_PUT(self):
        # Uncomment below to drain the request payload and prevent this issue
        # content_length = int(self.headers.get("Content-Length", 0))
        # if content_length:
        #     self.rfile.read(content_length)  # Uncomment to prevent connection reset

        target_url = urllib.parse.urljoin(REDIRECT_BASE_URL, self.path)

        self.send_response(307)
        self.send_header("Location", target_url)
        self.send_header("Content-Length", "0")
        self.end_headers()

if __name__ == "__main__":
    PORT = 5000
    with socketserver.TCPServer(("", PORT), ProxyHandler) as httpd:
        context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        context.load_cert_chain(certfile=CERT_FILE, keyfile=KEY_FILE)
        httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
        httpd.serve_forever()

SSL Certificate Generation

If needed, generate self-signed TLS certificates using the following OpenSSL commands:

openssl req -x509 -newkey rsa:2048 -keyout ca.key -out ca.crt -days 1024 -nodes -subj "/CN=localhost" \
  -extensions v3_ca -config <(printf "[req]\ndistinguished_name=req\nx509_extensions=v3_ca\n[ v3_ca ]\nsubjectAltName=DNS:localhost,DNS:127.0.0.1,IP:127.0.0.1\nbasicConstraints=CA:TRUE\n")

openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -subj "/C=US/ST=California/L=Santa Clara/O=COMPANY/OU=TEAM/CN=localhost" \
  -config <(printf "[req]\ndistinguished_name=req\nreq_extensions = v3_req\n[ v3_req ]\nsubjectAltName=DNS:localhost,DNS:127.0.0.1,IP:127.0.0.1\n")

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256 \
  -extfile <(printf "[ext]\nsubjectAltName=DNS:localhost,DNS:127.0.0.1,IP:127.0.0.1\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment\nextendedKeyUsage=serverAuth,clientAuth\n") -extensions ext

System Information

Python 3.10.12 (main, Jan 17 2025, 14:35:34) [GCC 11.4.0] on linux
$ python3 -m requests.help

{
  "chardet": {
    "version": null
  },
  "charset_normalizer": {
    "version": "3.4.1"
  },
  "cryptography": {
    "version": ""
  },
  "idna": {
    "version": "3.10"
  },
  "implementation": {
    "name": "CPython",
    "version": "3.10.12"
  },
  "platform": {
    "release": "5.15.0-118-generic",
    "system": "Linux"
  },
  "pyOpenSSL": {
    "openssl_version": "",
    "version": null
  },
  "requests": {
    "version": "2.32.3"
  },
  "system_ssl": {
    "version": "30000020"
  },
  "urllib3": {
    "version": "2.3.0"
  },
  "using_charset_normalizer": true,
  "using_pyopenssl": false
}
@aaronnw
Copy link

aaronnw commented Feb 4, 2025

Hi, wanted to add some background for this issue.

We noticed this on only Python versions 3.10-3.12. See these related issues:

Below is a screenshot of a pcap file for our specific SDK/server interaction. You can see the client receive a RST without sending a prior FIN packet.

Image

This seems to be some problem in handling the close_notify message from RFC-5246-7.2.1
https://datatracker.ietf.org/doc/html/rfc5246#section-7.2.1

In testing I observed this only happened with our Python SDK -- our similar GO SDK sends a FIN before following the redirect from the same server.

We implemented a workaround here: NVIDIA/aistore@06c0881

This strips the data from the request to our proxy service and opens a new session for the request with data to the target. This works, but I believe is less than optimal since we have to create a new requests session.

@sigmavirus24
Copy link
Contributor

This strips the data from the request to our proxy service and opens a new session for the request with data to the target. This works, but I believe is less than optimal since we have to create a new requests session.

Why do you believe you have to create a new session?

@aaronnw
Copy link

aaronnw commented Feb 5, 2025

This strips the data from the request to our proxy service and opens a new session for the request with data to the target. This works, but I believe is less than optimal since we have to create a new requests session.

Why do you believe you have to create a new session?

I was actually wondering that myself looking back at it 😆. I may have left it from a previous attempt at a workaround. Testing it out, that part seems to not actually be necessary unless I'm missing something.

Still, the fix of stripping out the data and manually parsing the redirect should probably not be necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants