{"id":16413595,"url":"https://github.com/lexsca/spoof","last_synced_at":"2026-04-05T23:07:34.730Z","repository":{"id":52579973,"uuid":"73516394","full_name":"lexsca/spoof","owner":"lexsca","description":"On-demand HTTP test server for Python","archived":false,"fork":false,"pushed_at":"2022-11-06T21:57:37.000Z","size":262,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-01T22:51:38.007Z","etag":null,"topics":["http","http-proxy","http-server","python","testing","testing-tools"],"latest_commit_sha":null,"homepage":"https://spoof.readthedocs.io","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lexsca.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGELOG.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-11-11T22:30:47.000Z","updated_at":"2024-02-06T18:34:52.000Z","dependencies_parsed_at":"2022-09-02T19:52:31.129Z","dependency_job_id":null,"html_url":"https://github.com/lexsca/spoof","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lexsca%2Fspoof","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lexsca%2Fspoof/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lexsca%2Fspoof/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lexsca%2Fspoof/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lexsca","download_url":"https://codeload.github.com/lexsca/spoof/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244280907,"owners_count":20427757,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","http-proxy","http-server","python","testing","testing-tools"],"created_at":"2024-10-11T06:51:51.657Z","updated_at":"2026-04-02T12:50:53.164Z","avatar_url":"https://github.com/lexsca.png","language":"Python","readme":"########\nSpoof 👻\n########\n\n\n.. image:: https://github.com/lexsca/spoof/actions/workflows/checks.yml/badge.svg\n    :target: https://github.com/lexsca/spoof/actions/workflows/checks.yml\n.. image:: https://img.shields.io/pypi/v/spoof.svg\n    :target: https://pypi.org/project/spoof/\n.. image:: https://img.shields.io/pypi/pyversions/spoof.svg\n    :target: https://pypi.org/project/spoof/\n.. image:: https://img.shields.io/github/license/lexsca/spoof.svg\n    :target: https://github.com/lexsca/spoof/blob/master/LICENSE\n.. image:: https://img.shields.io/badge/code%20style-black-000000.svg\n    :target: https://github.com/psf/black\n\n|\n\n**Spoof** is a simple HTTP server for test environments.\n\n.. code-block:: python\n\n   \u003e\u003e\u003e import requests\n   ... import spoof\n   ...\n   ... with spoof.HTTPServer() as httpd:\n   ...     httpd.queueResponse([200, [], \"This is Spoof 👻👋\"])\n   ...     requests.get(httpd.url).text\n   ...     httpd.requests\n   ...\n   'This is Spoof 👻👋'\n   [SpoofRequestEnv(method='GET', uri='/', protocol='HTTP/1.1', serverName='localhost', serverPort=62775, headers=\u003chttp.client.HTTPMessage object at 0x10d8a8f50\u003e, path='/', queryString=None, content=None, contentType=None, contentEncoding=None, contentLength=0)]\n\nSpoof lets you easily create HTTP servers listening on *real* network\nsockets. Designed for test environments, what responses to return can be\nconfigured while an HTTP server is running, and requests can be inspected\nlive or after a response is sent.\n\nUnlike a traditional HTTP server, where specific methods and paths are\nconfigured in advance, Spoof accepts and captures *all* requests, sending\nwhatever responses are queued, or a default response if the queue is empty.\n\nBut why?\n========\n\nSpoof is all about enabling test-driven development (and refactoring) of\nclient libraries. Have you ever felt icky patching a client library to\nwrite tests? Ever been burned by this? Ever wanted to refactor a client\nlibrary, but had no way to prove functionality apart from doing live\nintegration testing? If you answered yes to any of the above, Spoof is\nfor you.\n\nCompatibility\n=============\n\nSpoof is tested on Python 3.10 to 3.14, and has no external dependencies. It may\nalso work on older versions of Python, but this is not supported.\n\nMultiple Spoof HTTP servers can be run concurrently, and by default, the port\nnumber is the next available unused port.  With OpenSSL installed, Spoof can\nalso provide an SSL/TLS HTTP server.  IPv6 is fully supported.\n\n`SpoofRequestEnv` instances\n===========================\n\nSpoof captures each request as a `namedtuple` with the following properties:\n\n+-------------------------+----------------------------------------------+\n| Property                | Description                                  |\n+=========================+==============================================+\n| content                 | `bytes` object of request content            |\n+-------------------------+----------------------------------------------+\n| contentEncoding         | Value of Content-Encoding header, if present |\n+-------------------------+----------------------------------------------+\n| contentLength           | Value of Content-Length header, if present   |\n+-------------------------+----------------------------------------------+\n| contentType             | Value of Content-Type header, if present     |\n+-------------------------+----------------------------------------------+\n| headers                 | `http.client.HTTPMessage` object of headers  |\n+-------------------------+----------------------------------------------+\n| method                  | Request method (e.g. GET, POST, HEAD)        |\n+-------------------------+----------------------------------------------+\n| path                    | Decoded URI path, without query string       |\n+-------------------------+----------------------------------------------+\n| protocol                | Protocol version (e.g. HTTP/1.0)             |\n+-------------------------+----------------------------------------------+\n| queryString             | Anything in URI after `?`                    |\n+-------------------------+----------------------------------------------+\n| serverName              | Host name of HTTP server                     |\n+-------------------------+----------------------------------------------+\n| serverPort              | Port number of HTTP server                   |\n+-------------------------+----------------------------------------------+\n| uri                     | Raw URI path and query string, if present    |\n+-------------------------+----------------------------------------------+\n\nQueued responses\n================\n\nQueue multiple responses, verify content, and request paths:\n\n.. code-block:: python\n\n   import requests\n   import spoof\n\n   with spoof.HTTPServer() as httpd:\n       responses = [\n           [200, [(\"Content-Type\", \"application/json\")], '{\"id\": 1111}'],\n           [200, [(\"Content-Type\", \"application/json\")], '{\"id\": 2222}'],\n       ]\n       httpd.queueResponse(*responses)\n       httpd.defaultResponse = [404, [], \"Not found\"]\n\n       assert requests.get(httpd.url + \"/path\").json() == {\"id\": 1111}\n       assert requests.get(httpd.url + \"/alt/path\").json() == {\"id\": 2222}\n       assert requests.get(httpd.url + \"/oops\").status_code == 404\n       assert [r.path for r in httpd.requests] == [\"/path\", \"/alt/path\", \"/oops\"]\n\nCallback response\n=================\n\nSet a callback as the default response (callbacks can also be queued):\n\n.. code-block:: python\n\n   import requests\n   import spoof\n\n   with spoof.HTTPServer() as httpd:\n       httpd.defaultResponse = lambda request: [200, [], request.path]\n\n       assert requests.get(httpd.url + \"/alt\").content == b\"/alt\"\n\nSSL/TLS Mode\n============\n\nTest queued response with a self-signed SSL/TLS certificate:\n\n.. code-block:: python\n\n   import requests\n   import spoof\n\n   with spoof.SelfSignedSSLContext() as selfSigned:\n       with spoof.HTTPServer(sslContext=selfSigned.sslContext) as httpd:\n           httpd.queueResponse([200, [], \"No self-signed cert warning!\"])\n           response = requests.get(httpd.url + \"/path\",\n                                   verify=selfSigned.certFile)\n\n           assert httpd.requests[-1].method == \"GET\"\n           assert httpd.requests[-1].path == \"/path\"\n           assert response.content == b\"No self-signed cert warning!\"\n\nProxy Mode\n==========\n\nSpoof also supports proxying HTTP requests by setting the ``upstream`` attribute\nto another Spoof instance:\n\n.. code-block:: python\n\n   import requests\n   import spoof\n\n   with spoof.SelfSignedSSLContext(commonName=\"example.spoof\") as ssl:\n       with spoof.HTTPServer(sslContext=ssl.sslContext) as proxy:\n           with spoof.HTTPServer(sslContext=ssl.sslContext) as upstream:\n               proxy.upstream = upstream\n               proxy.defaultResponse = [200, [(\"X-Spoof-Proxy\", \"True\")], \"\"]\n               upstream.defaultResponse = [200, [], \"I'm here!\"]\n               response = requests.get(\n                   \"https://example.spoof/ayt\",\n                   proxies={\"https\": proxy.url},\n                   verify=ssl.certFile\n               )\n               assert proxy.requests[0].method == \"CONNECT\"\n               assert proxy.requests[0].path == \"example.spoof:443\"\n               assert upstream.requests[0].method == \"GET\"\n               assert upstream.requests[0].path == \"/ayt\"\n               assert response.content == b\"I'm here!\"\n\nUsing IPv6\n==========\n\nSetting the `host` attribute to an IPv6 address will work as expected. There is\nalso an IPv6-only `spoof.HTTPServer6` class that can be used if needed.\n\n.. code-block:: python\n\n   \u003e\u003e\u003e import requests\n   ... import spoof\n   ...\n   ... with spoof.HTTPServer(host=\"::1\") as httpd:\n   ...     httpd.queueResponse([200, [], \"This is Spoof on IPv6 👀\"])\n   ...     requests.get(httpd.url).text\n   ...     httpd.url\n   ...\n   'This is Spoof on IPv6 👀'\n   'http://[::1]:51324'\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flexsca%2Fspoof","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flexsca%2Fspoof","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flexsca%2Fspoof/lists"}