From 27f04b35442b5a6b9015f17bab46b7df3371f3fc Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Mon, 7 Aug 2023 10:48:35 +0200 Subject: Picklable HTTPError (#39285) --- lib/spack/spack/test/web.py | 25 +++++++++++++++++++++++++ lib/spack/spack/util/web.py | 13 +++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/spack/spack/test/web.py b/lib/spack/spack/test/web.py index 89023875df..2d6f577799 100644 --- a/lib/spack/spack/test/web.py +++ b/lib/spack/spack/test/web.py @@ -3,7 +3,10 @@ # # SPDX-License-Identifier: (Apache-2.0 OR MIT) import collections +import email.message import os +import pickle +import urllib.request import pytest @@ -339,3 +342,25 @@ def test_s3_url_exists(monkeypatch, capfd): def test_s3_url_parsing(): assert spack.util.s3._parse_s3_endpoint_url("example.com") == "https://example.com" assert spack.util.s3._parse_s3_endpoint_url("http://example.com") == "http://example.com" + + +def test_detailed_http_error_pickle(tmpdir): + tmpdir.join("response").write("response") + + headers = email.message.Message() + headers.add_header("Content-Type", "text/plain") + + # Use a temporary file object as a response body + with open(str(tmpdir.join("response")), "rb") as f: + error = spack.util.web.DetailedHTTPError( + urllib.request.Request("http://example.com"), 404, "Not Found", headers, f + ) + + deserialized = pickle.loads(pickle.dumps(error)) + + assert isinstance(deserialized, spack.util.web.DetailedHTTPError) + assert deserialized.code == 404 + assert deserialized.filename == "http://example.com" + assert deserialized.reason == "Not Found" + assert str(deserialized.info()) == str(headers) + assert str(deserialized) == str(error) diff --git a/lib/spack/spack/util/web.py b/lib/spack/spack/util/web.py index ffaa54d012..22309ba87f 100644 --- a/lib/spack/spack/util/web.py +++ b/lib/spack/spack/util/web.py @@ -17,6 +17,7 @@ import traceback import urllib.parse from html.parser import HTMLParser from pathlib import Path, PurePosixPath +from typing import IO, Optional from urllib.error import HTTPError, URLError from urllib.request import HTTPSHandler, Request, build_opener @@ -40,7 +41,9 @@ from spack.util.path import convert_to_posix_path class DetailedHTTPError(HTTPError): - def __init__(self, req: Request, code: int, msg: str, hdrs: email.message.Message, fp) -> None: + def __init__( + self, req: Request, code: int, msg: str, hdrs: email.message.Message, fp: Optional[IO] + ) -> None: self.req = req super().__init__(req.get_full_url(), code, msg, hdrs, fp) @@ -48,7 +51,13 @@ class DetailedHTTPError(HTTPError): # Note: HTTPError, is actually a kind of non-seekable response object, so # best not to read the response body here (even if it may include a human-readable # error message). - return f"{self.req.get_method()} {self.url} returned {self.code}: {self.msg}" + # Note: use self.filename, not self.url, because the latter requires fp to be an + # IO object, which is not the case after unpickling. + return f"{self.req.get_method()} {self.filename} returned {self.code}: {self.msg}" + + def __reduce__(self): + # fp is an IO object and not picklable, the rest should be. + return DetailedHTTPError, (self.req, self.code, self.msg, self.hdrs, None) class SpackHTTPDefaultErrorHandler(urllib.request.HTTPDefaultErrorHandler): -- cgit v1.2.3-60-g2f50