1"use strict";(globalThis.webpackChunklanding_blog=globalThis.webpackChunklanding_blog||[]).push([[8134],{39356(e,t,r){r.r(t),r.d(t,{assets:()=>l,contentTitle:()=>o,default:()=>d,frontMatter:()=>a,metadata:()=>n,toc:()=>c});var n=r(32437),s=r(74848),i=r(28453);const a={slug:"python-proxy-server-proxy-py",title:"How to Create a Proxy Server in Python Using Proxy.py",description:"Run a local Python proxy with proxy.py. Test HTTP, HTTPS CONNECT, authentication, header plugins and upstream pooling with reproducible examples.",author:"Oleg Kulyk",author_title:"Co-Founder @ ScrapingAnt",author_url:"https://www.linkedin.com/in/kami4ka/",author_image_url:"https://avatars0.githubusercontent.com/u/5595029?s=400&v=4",image:"/img/blog/python-proxy-server-proxy-py.jpg",tags:["web scraping","data extraction","python"],last_update:{date:new Date("2026-09-22T00:00:00.000Z"),author:"Oleg Kulyk"}},o=void 0,l={authorsImageUrls:[void 0]},c=[{value:"Install proxy.py and start the local lab",id:"install-proxypy-and-start-the-local-lab",level:2},{value:"Send an HTTP request through the proxy",id:"send-an-http-request-through-the-proxy",level:2},{value:"Use Python Requests",id:"use-python-requests",level:3},{value:"Forward HTTPS with CONNECT, without interception",id:"forward-https-with-connect-without-interception",level:2},{value:"Require proxy authentication",id:"require-proxy-authentication",level:2},{value:"Add a small HTTP header plugin",id:"add-a-small-http-header-plugin",level:2},{value:"Route through an upstream pool",id:"route-through-an-upstream-pool",level:2},{value:"Read failures and stop cleanly",id:"read-failures-and-stop-cleanly",level:2},{value:"What was verified, and when a remote proxy is relevant",id:"what-was-verified-and-when-a-remote-proxy-is-relevant",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"How to Create a Proxy Server in Python Using Proxy.py",src:r(41853).A+"",width:"1456",height:"816"})}),"\n",(0,s.jsx)(t.admonition,{title:"Updated 2026-09-22",type:"info",children:(0,s.jsxs)(t.p,{children:["Re-tested HTTP forwarding, HTTPS CONNECT, authentication and plugins with proxy.py 2.4.10. Replaced the broken rotation recipe with a tested upstream pool. HTTPS forwarding does not require TLS interception, and local proxy ports do not establish different public exit IPs. ",(0,s.jsx)(t.a,{href:"https://github.com/ScrapingAnt/scrapingant-examples/tree/53aea6a71ce869c648f1fb31dcf8c384a73bb6fd/examples/python-proxy-server-proxy-py",children:"Commands, fixtures and captured logs"}),"."]})}),"\n",(0,s.jsx)(t.p,{children:"To create a local Python proxy server, start proxy.py on an explicit loopback address and point your client at it. Then verify both the response and the proxy log: receiving a page is not enough to prove which route the client used."}),"\n",(0,s.jsx)(t.p,{children:"This guide builds that path with local HTTP and HTTPS origins, cURL and Requests clients, authentication, a header plugin and an upstream pool. You can run the complete lab without an external target, a ScrapingAnt account or an API key."}),"\n",(0,s.jsx)(t.h2,{id:"install-proxypy-and-start-the-local-lab",children:"Install proxy.py and start the local lab"}),"\n",(0,s.jsxs)(t.p,{children:["The recorded environment was Python 3.12.11 on macOS 26.6.2, with proxy.py 2.4.10, Requests 2.34.2 and cURL 8.7.1. Use Git, Python 3.12, Bash, cURL and OpenSSL for this reproduction. Keep loopback ports ",(0,s.jsx)(t.strong,{children:"8000, 8443 and 8899\u20138905"})," free."]}),"\n",(0,s.jsx)(t.p,{children:"Get the pinned packet and install its dependencies:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"git clone https://github.com/ScrapingAnt/scrapingant-examples.git\ncd scrapingant-examples\ngit checkout 53aea6a71ce869c648f1fb31dcf8c384a73bb6fd\ncd examples/python-proxy-server-proxy-py\npython3 -m venv .venv\nsource .venv/bin/activate\npython -m pip install -r requirements.txt\npython lab.py serve\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The requirements file pins proxy.py and Requests, including their installed dependencies. Wait for the ",(0,s.jsx)(t.code,{children:"READY"})," line. The helper creates two JSON origins and the proxy listeners used below:"]}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Address"}),(0,s.jsx)(t.th,{children:"Purpose"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://localhost:8000"})}),(0,s.jsx)(t.td,{children:"HTTP origin"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"https://localhost:8443"})}),(0,s.jsx)(t.td,{children:"HTTPS origin with a generated test certificate"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://127.0.0.1:8899"})}),(0,s.jsx)(t.td,{children:"Plain forward proxy"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://127.0.0.1:8900"})}),(0,s.jsx)(t.td,{children:"Authenticated proxy"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.code,{children:"http://127.0.0.1:8901"})," and ",(0,s.jsx)(t.code,{children:":8902"})]}),(0,s.jsx)(t.td,{children:"Two local upstream proxies"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://127.0.0.1:8903"})}),(0,s.jsx)(t.td,{children:"Pool proxy"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"http://127.0.0.1:8904"})}),(0,s.jsx)(t.td,{children:"Header-plugin proxy"})]})]})]}),"\n",(0,s.jsxs)(t.p,{children:["Port 8905 is reserved for reproducing the old recipe'
1s failure. Keep the lab running in this terminal. In a second terminal, enter the same packet directory and activate ",(0,s.jsx)(t.code,{children:".venv"})," before running the clients."]}),"\n",(0,s.jsxs)(t.p,{children:["For reference, this is the plain proxy's startup command, with the captured virtualenv executable path shortened to ",(0,s.jsx)(t.code,{children:"python"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"python -m proxy --hostname 127.0.0.1 --port 8899 --num-workers 1 --num-acceptors 1 --log-level INFO\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The helper already starts that process; do not start it again while the lab is running. An explicit ",(0,s.jsx)(t.code,{children:"127.0.0.1"})," bind keeps this example on loopback. The initial log confirms the HTTP proxy plugin loaded; the access log appears after a client request."]}),"\n",(0,s.jsx)(t.h2,{id:"send-an-http-request-through-the-proxy",children:"Send an HTTP request through the proxy"}),"\n",(0,s.jsx)(t.p,{children:"Run this cURL command from the packet directory:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"curl --noproxy '' --silent --show-error --fail --verbose --max-time 5 --proxy http://127.0.0.1:8899 http://localhost:8000/curl-False\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured response body:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{"marker": null, "origin": "proxy-py-local-fixture", "path": "/curl-False", "tls": false}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.code,{children:"origin"})," identifies our fixture, and ",(0,s.jsx)(t.code,{children:"path"})," identifies this request. The matching line in ",(0,s.jsx)(t.code,{children:"expected_output/plain.log"})," was:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:"2026-09-22 20:24:19,075 - pid:9686 [I] server.access_log:393 - 127.0.0.1:60456 - GET localhost:8000/curl-False - 200 OK - 235 bytes - 1.51ms\n"})}),"\n",(0,s.jsx)(t.p,{children:"Your timestamp, process ID, client port and duration will differ. The destination and request path are what connect the client output to the proxy log. The recorded duration is a log field, not a performance benchmark."}),"\n",(0,s.jsxs)(t.p,{children:["Use ",(0,s.jsx)(t.code,{children:"--noproxy ''"})," even for this local target so environment bypass settings cannot silently exclude it. The runner also clears inherited proxy environment variables. For more client-side options, see ",(0,s.jsx)(t.a,{href:"/blog/curl-with-proxy",children:"using cURL with a proxy"}),"."]}),"\n",(0,s.jsx)(t.h3,{id:"use-python-requests",children:"Use Python Requests"}),"\n",(0,s.jsxs)(t.p,{children:["The packet includes this complete ",(0,s.jsx)(t.code,{children:"client.py"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-python",children:"\"\"\"Usage: python client.py [http|https]; start `python lab.py serve` first.\"\"\"\nimport sys\nimport requests\n\nscheme = sys.argv[1] if len(sys.argv) > 1 else 'http'\nif scheme not in {'http', 'https'}:\n raise SystemExit('Use http or https')\nurl = f'{scheme}://localhost:{8443 if scheme == \"https\" else 8000}/reader-client'\nproxies = {'http': 'http://127.0.0.1:8899', 'https': 'http://127.0.0.1:8899'}\nwith requests.Session() as session:\n session.trust_env = False\n response = session.get(url, proxies=proxies, timeout=5, verify='generated/ca.pem')\n response.raise_for_status()\n print(response.status_code)\n print(response.text, end='')\n"})}),"\n",(0,s.jsx)(t.p,{children:"Run it for HTTP:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"python client.py http\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured output:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:'200\n{"marker": null, "origin": "proxy-py-local-fixture", "path": "/reader-client", "tls": false}\n'})}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.code,{children:"trust_env=False"})," makes the client use the explicit settings in this example, and the timeout bounds its connection/read waits. The ",(0,s.jsx)(t.code,{children:"https"})," dictionary key also points to an ",(0,s.jsx)(t.code,{children:"http://"})," proxy URL: it describes the proxy connection, not the destination's scheme. The ",(0,s.jsx)(t.a,{href:"/blog/python-requests-proxy",children:"Requests proxy guide"})," covers the client configuration separately."]}),"\n",(0,s.jsx)(t.h2,{id:"forward-https-with-connect-without-interception",children:"Forward HTTPS with CONNECT, without interception"}),"\n",(0,s.jsxs)(t.p,{children:["The lab generates a test CA and a certificate for ",(0,s.jsx)(t.code,{children:"localhost"}),". It passes that CA file to the clients explicitly; it does not install a CA into your system trust store. The proxy is started without interception certificate flags."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"curl --noproxy '' --silent --show-error --fail --verbose --max-time 5 --proxy http://127.0.0.1:8899 --cacert generated/ca.pem https://localhost:8443/curl-True\n"})}),"\n",(0,s.jsx)(t.p,{children:"These lines are excerpts from the captured verbose output:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:"> CONNECT localhost:8443 HTTP/1.1\n< HTTP/1.1 200 Connection established\n* SSL certificate verify ok.\n"})}),"\n",(0,s.jsx)(t.p,{children:"The final origin response was:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{"marker": null, "origin": "proxy-py-local-fixture", "path": "/curl-True", "tls": true}\n'})}),"\n",(0,s.jsx)(t.p,{children:"CONNECT establishes the tunnel; the client then performs TLS with the origin and verifies its certificate. This is enough for the demonstrated HTTPS request. Decrypting traffic at the proxy is a separate task and is outside this lab."}),"\n",(0,s.jsx)(t.p,{children:"The equivalent Requests run is:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"python client.py https\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured output:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:'200\n{"marker": null, "origin": "proxy-py-local-fixture", "path": "/reader-client", "tls": true}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The generated ",(0,s.jsx)(t.code,{children:"ca.pem"})," belongs to this local fixture. Keep certificate verification enabled; do not use this lab CA as a general solution for unrelated HTTPS sites."]}),"\n",(0,s.jsx)(t.h2,{id:"require-proxy-authentication",children:"Require proxy authentication"}),"\n",(0,s.jsxs)(t.p,{children:["The lab's port 8900 uses ",(0,s.jsx)(t.code,{children:"--basic-auth lab:secret"}),". These are disposable fixture credentials. Its complete startup configuration, again using the activated interpreter, is:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"python -m proxy --hostname 127.0.0.1 --p
1ort 8900 --num-workers 1 --num-acceptors 1 --log-level INFO --basic-auth lab:secret\n"})}),"\n",(0,s.jsx)(t.p,{children:"It is already running under the helper. Test it with:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"curl --noproxy '' --silent --show-error --fail --verbose --max-time 5 --proxy http://127.0.0.1:8900 --proxy-user lab:secret http://localhost:8000/auth-curl-False\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured body:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{"marker": null, "origin": "proxy-py-local-fixture", "path": "/auth-curl-False", "tls": false}\n'})}),"\n",(0,s.jsx)(t.p,{children:"To observe rejection, omit the credentials:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"curl --noproxy '' --silent --show-error --fail --verbose --max-time 5 --proxy http://127.0.0.1:8900 http://localhost:8000/rejected-False-None\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured error excerpt:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:"< HTTP/1.1 407 Proxy Authentication Required\ncurl: (22) The requested URL returned error: 407\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The runner also tests wrong credentials and both failure cases over HTTPS, with both clients. In this recorded cURL build, all four rejected cURL requests exited 22. Requests returned an HTTP response with status 407 for HTTP and raised ",(0,s.jsx)(t.code,{children:"ProxyError"})," containing ",(0,s.jsx)(t.code,{children:"Tunnel connection failed: 407 Proxy Authentication Required"})," for HTTPS."]}),"\n",(0,s.jsxs)(t.p,{children:["The rejected requests produced ",(0,s.jsx)(t.strong,{children:"zero origin hits across eight attempts"}),". Authentication success was checked separately for all four client/protocol combinations. These counts cover the declared test matrix; they are not a security audit."]}),"\n",(0,s.jsx)(t.h2,{id:"add-a-small-http-header-plugin",children:"Add a small HTTP header plugin"}),"\n",(0,s.jsxs)(t.p,{children:["A plugin should produce something you can observe at the destination. Here is the working class from ",(0,s.jsx)(t.code,{children:"lab_plugins.py"}),":"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-python",children:'class MarkerPlugin(HttpProxyBasePlugin):\n marker = b"local-lab"\n\n def before_upstream_connection(self, request):\n if not request.is_https_tunnel:\n request.add_header(b"X-Lab-Proxy", self.marker)\n return request\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The file imports ",(0,s.jsx)(t.code,{children:"HttpProxyBasePlugin"})," from ",(0,s.jsx)(t.code,{children:"proxy.http.proxy"}),". The helper makes the packet directory importable and loads the class on port 8904 with ",(0,s.jsx)(t.code,{children:"--plugins lab_plugins.MarkerPlugin"}),"."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"curl --noproxy '' --silent --show-error --fail --verbose --max-time 5 --proxy http://127.0.0.1:8904 http://localhost:8000/marker\n"})}),"\n",(0,s.jsx)(t.p,{children:"Captured body:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-json",children:'{"marker": "local-lab", "origin": "proxy-py-local-fixture", "path": "/marker", "tls": false}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The origin received the new header. For the HTTPS test through the same plugin, the origin returned ",(0,s.jsx)(t.code,{children:'"marker": null'}),": the plugin skips CONNECT and does not rewrite headers inside the encrypted tunnel."]}),"\n",(0,s.jsx)(t.h2,{id:"route-through-an-upstream-pool",children:"Route through an upstream pool"}),"\n",(0,s.jsxs)(t.p,{children:["The earlier version of this article selected a proxy by assigning its address to ",(0,s.jsx)(t.code,{children:"request.host"})," and ",(0,s.jsx)(t.code,{children:"request.port"}),". We retained that algorithm in the packet solely as a negative test, substituting the two fixture upstream ports. It returned HTTP 400 and never reached the intended origin."]}),"\n",(0,s.jsxs)(t.p,{children:["Use the release's ",(0,s.jsx)(t.code,{children:"ProxyPoolPlugin"})," for the demonstrated chain. Its ",(0,s.jsx)(t.a,{href:"https://github.com/abhinavsingh/proxy.py/blob/3b9964b683dccf4507380fd17d3403ac0cf64342/proxy/plugin/proxy_pool.py",children:"release-specific implementation"})," opens a separate upstream connection and builds the request for that proxy."]}
1),"\n",(0,s.jsx)(t.p,{children:"The helper starts upstreams on 8901 and 8902 first. Each adds a distinct marker to plain HTTP requests. It then starts the pool with this configuration:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"python -m proxy --hostname 127.0.0.1 --port 8903 --num-workers 1 --num-acceptors 1 --log-level INFO --plugins proxy.plugin.ProxyPoolPlugin --proxy-pool 127.0.0.1:8901 --proxy-pool 127.0.0.1:8902\n"})}),"\n",(0,s.jsxs)(t.p,{children:["For this pool test, keep the destination hostname ",(0,s.jsx)(t.code,{children:"localhost"}),". In proxy.py 2.4.10, a ",(0,s.jsx)(t.strong,{children:"literal private IP destination bypasses the pool"}),". The packet tests that difference: ",(0,s.jsx)(t.code,{children:"http://127.0.0.1:8000/private-ip-bypass"})," reaches the origin without either upstream marker."]}),"\n",(0,s.jsxs)(t.p,{children:["To reproduce the batch, stop ",(0,s.jsx)(t.code,{children:"serve"})," with Ctrl-C first, then run:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"./run.sh\n"})}),"\n",(0,s.jsx)(t.p,{children:"This starts its own listeners, checks each upstream individually and sends 20 pool requests using fresh Requests sessions. Every response must have the expected origin, request path and upstream marker. The selected upstream's access log must also contain that path."}),"\n",(0,s.jsx)(t.p,{children:"Captured pool summary:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-text",children:'POOL: {"upstream-a": 10, "upstream-b": 10}; correct destinations=20/20\n'})}),"\n",(0,s.jsx)(t.p,{children:"That split is an observation from this run. The plugin selects randomly when a client connection is created; it does not promise round-robin selection or a different endpoint on each request over a reused connection. Your split can differ. Both upstreams here run on one machine, so this proves routing through two ports, not rotation across different public IP addresses. No failover or throughput claim follows from these results."}),"\n",(0,s.jsx)(t.h2,{id:"read-failures-and-stop-cleanly",children:"Read failures and stop cleanly"}),"\n",(0,s.jsx)(t.p,{children:"The packet asserts failures as well as successes. A traceback in the captured negative case is expected; the overall runner still exits zero only when its assertions pass."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Symptom"}),(0,s.jsx)(t.th,{children:"Observed case or next check"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cURL exit 7, connection refused"}),(0,s.jsx)(t.td,{children:"The controlled unavailable-listener case. Check that your proxy started and that the client's host/port match it."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsxs)(t.td,{children:[(0,s.jsx)(t.code,{children:"Address already in use"}),", proxy process exit 1"]}),(0,s.jsx)(t.td,{children:"The packet deliberately starts a second proxy on occupied port 8899. Stop your earlier lab process or free the required port."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsxs)(t.td,{children:["HTTP 407 or HTTPS ",(0,s.jsx)(t.code,{children:"ProxyError"})," mentioning 407"]}),(0,s.jsx)(t.td,{children:"Missing or incorrect proxy credentials in this lab. Check the proxy URL and authentication setting."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"A response arrives but no expected access log appears"}),(0,s.jsxs)(t.td,{children:["Check the explicit proxy address and bypass settings. Match the request path against ",(0,s.jsx)(t.code,{children:"plain.log"})," and ",(0,s.jsx)(t.code,{children:"origin.jsonl"}),"."]})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"The HTTPS fixture fails after being left running"}),(0,s.jsx)(t.td,{children:"Its generated certificate is valid for two days. Stop and restart the lab to generate a fresh certificate; keep the matching CA file."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"The pool response has no upstream marker"}),(0,s.jsx)(t.td,{children:"Check whether the destination is a literal private IP; that branch bypasses this release's pool."})]})]})]}),"\n",(0,s.jsxs)(t.p,{children:["Press Ctrl-C in the ",(0,s.jsx)(t.code,{children:"serve"})," terminal when finished. The helper stops the subprocesses it started and closes the origins. The automated ",(0,s.jsx)(t.code,{children:"run.sh"})," does that cleanup itself. Do not run ",(0,s.jsx)(t.code,{children:"serve"})," and ",(0,s.jsx)(t.code,{children:"run.sh"})," together: they use the same ports and output files."]}),"\n",(0,s.jsx)(t.h2,{id:"what-was-verified-and-when-a-remote-proxy-is-relevant",children:"What was verified, and when a remote proxy is relevant"}),"\n",(0,s.jsx)(t.p,{children:"The recorded test matrix was:"}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Check"}),(0,s.jsx)(t.th,{children:"Result"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"cURL and Requests \xd7 HTTP and HTTPS"}),(0,s.jsx)(t.td,{children:"4 successful origin responses / 4 attempts"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Same matrix with correct proxy credentials"}),(0,s.jsx)(t.td,{children:"4 successful origin responses / 4 attempts"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Missing/wrong credentials \xd7 both clients \xd7 both protocols"}),(0,s.jsx)(t.td,{children:"8 rejections / 8 attempts; 0 origin hits"})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"Pool requests over fresh client connections"}),(0,s.jsx)(t.td,{children:"20 correct destinations / 20 attempts"})]})]})]}),"\n",(0,s.jsx)(t.p,{children:"There are additional reader-script, plugin, individual-upstream, private-IP and tunnel cases in the packet. This table is not a total count of every request."}),"\n",(0,s.jsxs)(t.p,{children:["ScrapingAnt is not needed to run this local lab. If your task instead requires a separately operated upstream, the next step is the ",(0,s.jsx)(t.a,{href:"https://proxydocs.scrapingant.com/datacenter/setup",children:"ScrapingAnt datacenter proxy setup documentation"}
1),". No paid service was called in these tests. For the broader choice of proxy types, see ",(0,s.jsx)(t.a,{href:"/blog/proxies-for-web-scraping",children:"proxies for web scraping"}),"."]}),"\n",(0,s.jsx)(t.p,{children:"The evidence covers the pinned release and local fixtures. It does not demonstrate SOCKS support, production capacity, anonymity, CAPTCHA handling or avoidance of target-site blocks. Review the packet again when changing proxy.py versions or plugin behavior."}),"\n",(0,s.jsxs)(t.p,{children:["Examples tested on 2026-09-22 with Python 3.12.11, proxy.py 2.4.10, Requests 2.34.2 and cURL 8.7.1. Code: ",(0,s.jsx)(t.a,{href:"https://github.com/ScrapingAnt/scrapingant-examples/tree/53aea6a71ce869c648f1fb31dcf8c384a73bb6fd/examples/python-proxy-server-proxy-py",children:"reproducible proxy.py packet and raw logs"}),"."]}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.em,{children:"This article was drafted with AI assistance from a tested evidence packet and reviewed by the named author, who is responsible for the code, measurements and corrections."})})]})}function d(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},41853(e,t,r){r.d(t,{A:()=>n});const n=r.p+"assets/images/python-proxy-server-proxy-py-29c84a9e21442e7b56f1efa3c959fe89.jpg"},28453(e,t,r){r.d(t,{R:()=>a,x:()=>o});var n=r(96540);const s={},i=n.createContext(s);function a(e){const t=n.useContext(i);return n.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),n.createElement(i.Provider,{value:t},e.children)}},32437(e){e.exports=JSON.parse('{"permalink":"/blog/python-proxy-server-proxy-py","source":"@site/blog/2023-11-22-python-proxy-server-proxy-py.md","title":"How to Create a Proxy Server in Python Using Proxy.py","description":"Run a local Python proxy with proxy.py. Test HTTP, HTTPS CONNECT, authentication, header plugins and upstream pooling with reproducible examples.","date":"2023-11-22T00:00:00.000Z","tags":[{"inline":true,"label":"web scraping","permalink":"/blog/tags/web-scraping"},{"inline":true,"label":"data extraction","permalink":"/blog/tags/data-extraction"},{"inline":true,"label":"python","permalink":"/blog/tags/python"}],"readingTime":10.63,"hasTruncateMarker":true,"authors":[{"name":"Oleg Kulyk","title":"Co-Founder @ ScrapingAnt","url":"https://www.linkedin.com/in/kami4ka/","imageURL":"https://avatars0.githubusercontent.com/u/5595029?s=400&v=4","key":null,"page":null}],"frontMatter":{"slug":"python-proxy-server-proxy-py","title":"How to Create a Proxy Server in Python Using Proxy.py","description":"Run a local Python proxy with proxy.py. Test HTTP, HTTPS CONNECT, authentication, header plugins and upstream pooling with reproducible examples.","author":"Oleg Kulyk","author_title":"Co-Founder @ ScrapingAnt","author_url":"https://www.linkedin.com/in/kami4ka/","author_image_url":"https://avatars0.githubusercontent.com/u/5595029?s=400&v=4","image":"/img/blog/python-proxy-server-proxy-py.jpg","tags":["web scraping","data extraction","python"],"last_update":{"date":"2026-09-22T00:00:00.000Z","author":"Oleg Kulyk"}},"unlisted":false,"lastUpdatedAt":1790035200000,"lastUpdatedBy":"Oleg Kulyk","prevItem":{"title":"How to Save Up to 40% of Data Collection Budgets with a Web Scraping API","permalink":"/blog/cost-efficient-web-scraping-api"},"nextItem":{"title":"How to Use Requests Library with Sessions to Crawl Websites in Python","permalink":"/blog/request-session-crawling"}}')}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.