{ "info": { "author": "Steinn Eldj\u00e1rn Sigur\u00f0arson", "author_email": "steinnes@gmail.com", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "# content size limit\n\n[![CircleCI](https://circleci.com/gh/steinnes/content-size-limit-asgi.svg?style=svg)](https://circleci.com/gh/steinnes/content-size-limit-asgi)\n\nThis is a middleware for ASGI which intercepts the receive() method to raise\nan exception when the read bytes exceed the given limit.\n\n## Example\n\n```python\nimport uvicorn\n\nfrom starlette.applications import Starlette\nfrom starlette.responses import PlainTextResponse\n\nfrom content_size_limit_asgi import ContentSizeLimitMiddleware\n\n\napp = Starlette()\n\n@app.route(\"/\", methods=[\"POST\"])\nasync def index(request):\n body = await request.body()\n return PlainTextResponse(f\"body: {body.decode('utf-8')}\")\n\n\napp.add_middleware(ContentSizeLimitMiddleware, max_content_size=512)\n\n\nif __name__ == \"__main__\":\n uvicorn.run(app, host=\"127.0.0.1\", port=6001, log_level='debug')\n```\n\nTo test the app:\n\n```\n$ curl --limit-rate 5k -q -v http://localhost:6001/ -d `printf 'A%.0s' {1..99999}`\n* Trying 127.0.0.1...\n* TCP_NODELAY set\n* Connected to localhost (127.0.0.1) port 6001 (#0)\n> POST / HTTP/1.1\n> Host: localhost:6001\n> User-Agent: curl/7.54.0\n> Accept: */*\n> Content-Length: 99999\n> Content-Type: application/x-www-form-urlencoded\n> Expect: 100-continue\n>\n< HTTP/1.1 100 Continue\n< HTTP/1.1 500 Internal Server Error\n< date: Wed, 12 Jun 2019 14:41:28 GMT\n< server: uvicorn\n< content-length: 21\n< content-type: text/plain; charset=utf-8\n* HTTP error before end of send, stop sending\n<\n* Closing connection 0\nInternal Server Error\n```\n\nThe app console log should read:\n```\n$ PYTHONPATH=. python tapp.py\nINFO: Started server process [48160]\nINFO: Waiting for application startup.\nDEBUG: None - ASGI [1] Started\nWARNING 2019-06-12 14:42:18,003 content_size_limit.middleware: ASGI scope of type lifespan is not supported yet\nWARNING: ASGI scope of type lifespan is not supported yet\nDEBUG: None - ASGI [1] Sent {'type': 'lifespan.startup'}\nDEBUG: None - ASGI [1] Received {'type': 'lifespan.startup.complete'}\nINFO: Uvicorn running on http://127.0.0.1:6001 (Press CTRL+C to quit)\nDEBUG: ('127.0.0.1', 52103) - Connected\nDEBUG: ('127.0.0.1', 52103) - ASGI [2] Started\nDEBUG: ('127.0.0.1', 52103) - ASGI [2] Sent {'type': 'http.request', 'body': '<16384 bytes>', 'more_body': True}\nDEBUG: ('127.0.0.1', 52103) - ASGI [2] Received {'type': 'http.response.start', 'status': 500, 'headers': '<...>'}\nINFO: ('127.0.0.1', 52103) - \"POST / HTTP/1.1\" 500\nDEBUG: ('127.0.0.1', 52103) - ASGI [2] Received {'type': 'http.response.body', 'body': '<21 bytes>'}\nDEBUG: ('127.0.0.1', 52103) - ASGI [2] Raised exception\nERROR: Exception in ASGI application\nTraceback (most recent call last):\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/uvicorn/protocols/http/httptools_impl.py\", line 368, in run_asgi\n result = await app(self.scope, self.receive, self.send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/uvicorn/middleware/message_logger.py\", line 58, in __call__\n raise exc from None\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/uvicorn/middleware/message_logger.py\", line 54, in __call__\n await self.app(scope, inner_receive, inner_send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/applications.py\", line 133, in __call__\n await self.error_middleware(scope, receive, send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/middleware/errors.py\", line 122, in __call__\n raise exc from None\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/middleware/errors.py\", line 100, in __call__\n await self.app(scope, receive, _send)\n File \"/Users/ses/w/content-size-limit-asgi/content_size_limit/middleware.py\", line 48, in __call__\n await self.app(scope, wrapper, send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/exceptions.py\", line 73, in __call__\n raise exc from None\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/exceptions.py\", line 62, in __call__\n await self.app(scope, receive, sender)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/routing.py\", line 585, in __call__\n await route(scope, receive, send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/routing.py\", line 207, in __call__\n await self.app(scope, receive, send)\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/routing.py\", line 40, in app\n response = await func(request)\n File \"tapp.py\", line 13, in index\n body = await request.body()\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/requests.py\", line 167, in body\n async for chunk in self.stream():\n File \"/Users/ses/.pyenv/versions/3.7.3/lib/python3.7/site-packages/starlette/requests.py\", line 152, in stream\n message = await self._receive()\n File \"/Users/ses/w/content-size-limit-asgi/content_size_limit/middleware.py\", line 36, in inner\n f\"Maximum content size limit ({self.max_content_size}) exceeded ({received} bytes read)\"\ncontent_size_limit.errors.ContentSizeExceeded: Maximum content size limit (512) exceeded (16384 bytes read)\nDEBUG: ('127.0.0.1', 52103) - Disconnected\n```\n\n## Why not just raise in the route / view functon itself?\n\nDepending on the ASGI server/framework used, you might not have access to\nthe raw stream to stop reading immediately once the maximum content size\nhas been exceeded.\n\nTake this Starlette view for example:\n\n\n```python\n\n@app.route(\"/documents/upload\", methods=[\"POST\"])\ndef upload_document(request):\n data = await request.body()\n if len(data) > Config.MAX_FILE_SIZE:\n return api_400(\n f\"This file exceeds the maximum file size we support at this time ({Config.MAX_FILE_SIZE})\",\n code=MAX_FILE_SIZE_EXCEEDED,\n )\n ...\n```\n\nIf the maximum file size is 5MB, and the uploaded file was 50MB, then this\nimplementation reads the entire 50MB into memory before rejecting the\nrequest.\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "", "keywords": "", "license": "MIT", "maintainer": "Steinn Eldj\u00e1rn Sigur\u00f0arson", "maintainer_email": "steinnes@gmail.com", "name": "content-size-limit-asgi", "package_url": "https://pypi.org/project/content-size-limit-asgi/", "platform": "", "project_url": "https://pypi.org/project/content-size-limit-asgi/", "project_urls": null, "release_url": "https://pypi.org/project/content-size-limit-asgi/0.1.2/", "requires_dist": [ "starlette (>=0.12.0,<0.13.0)" ], "requires_python": ">=3.6,<4.0", "summary": "An ASGI3 middleware to implement maximum content size limits (mostly useful for HTTP uploads)", "version": "0.1.2" }, "last_serial": 5395753, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "6d7e5bd54cbd178356258eb5a18fcad3", "sha256": "dea833fafe7d422848ba0ea473248115d2e238d59b1aefe9fee0277e90806279" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "6d7e5bd54cbd178356258eb5a18fcad3", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6,<4.0", "size": 2834, "upload_time": "2019-06-12T16:18:02", "url": "https://files.pythonhosted.org/packages/2a/2a/ab8c789bf5ca24d07fc334ad115748cc8b0b151c09ed0ea67c90b8c70991/content_size_limit_asgi-0.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a35482fa94b7761a68388755ef4f881d", "sha256": "95a1f94f0a1806028af03f52c39a1462e692fd8c03e802dab02b95b1d76aaaf0" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.0.tar.gz", "has_sig": false, "md5_digest": "a35482fa94b7761a68388755ef4f881d", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6,<4.0", "size": 1964, "upload_time": "2019-06-12T16:18:05", "url": "https://files.pythonhosted.org/packages/69/24/43ceb3af60f9061cc13861e3d29f2fcb842c0313672b8b763d9efcbc47aa/content_size_limit_asgi-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "4c14054346b3f452e7a2e8aa268d194c", "sha256": "618bbd034f3d26f776095fb2b9cf3e1a555ce1df95d36239297d46f2cd8921de" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.1-py3-none-any.whl", "has_sig": false, "md5_digest": "4c14054346b3f452e7a2e8aa268d194c", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6,<4.0", "size": 2848, "upload_time": "2019-06-12T17:35:43", "url": "https://files.pythonhosted.org/packages/c3/21/ba54c234fd100582d0571e02a29dbda2017c928af9aa4de78b407793cbf7/content_size_limit_asgi-0.1.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "faef4022c2388c0d34774812c791f7da", "sha256": "284c8a855c3b2d6c0c7c5be23793bd91c473d10beaee0b72eefb88a4dc6e49dd" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.1.tar.gz", "has_sig": false, "md5_digest": "faef4022c2388c0d34774812c791f7da", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6,<4.0", "size": 1989, "upload_time": "2019-06-12T17:35:45", "url": "https://files.pythonhosted.org/packages/e6/0e/c35b122d205136e880c614c80052e29440336364a8f93ce5cd652f9d6562/content_size_limit_asgi-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "559ec53ddd66e91d22ab9c4c24d1b29f", "sha256": "e0ac94d62eb2923dd008ea4c6b9ecad9f91c32afcaf6294afb18bf6f4a88bea0" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "559ec53ddd66e91d22ab9c4c24d1b29f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6,<4.0", "size": 4867, "upload_time": "2019-06-13T12:42:20", "url": "https://files.pythonhosted.org/packages/5c/3b/0366bfa8cf938b88ce79538abeb703877b29a1638ef1f1bc26211fe0a8ea/content_size_limit_asgi-0.1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "97dd28b1e5fd5b02612d37f5c905b475", "sha256": "55134c1c1e5dc95d5409ee33d219e3756da6b50bb111c3c91a9b0571e21e707c" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.2.tar.gz", "has_sig": false, "md5_digest": "97dd28b1e5fd5b02612d37f5c905b475", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6,<4.0", "size": 4723, "upload_time": "2019-06-13T12:42:22", "url": "https://files.pythonhosted.org/packages/3e/c7/29019321d54b18624859c597cd39b5fca40c12f3fd74122f473b79b10117/content_size_limit_asgi-0.1.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "559ec53ddd66e91d22ab9c4c24d1b29f", "sha256": "e0ac94d62eb2923dd008ea4c6b9ecad9f91c32afcaf6294afb18bf6f4a88bea0" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.2-py3-none-any.whl", "has_sig": false, "md5_digest": "559ec53ddd66e91d22ab9c4c24d1b29f", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": ">=3.6,<4.0", "size": 4867, "upload_time": "2019-06-13T12:42:20", "url": "https://files.pythonhosted.org/packages/5c/3b/0366bfa8cf938b88ce79538abeb703877b29a1638ef1f1bc26211fe0a8ea/content_size_limit_asgi-0.1.2-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "97dd28b1e5fd5b02612d37f5c905b475", "sha256": "55134c1c1e5dc95d5409ee33d219e3756da6b50bb111c3c91a9b0571e21e707c" }, "downloads": -1, "filename": "content_size_limit_asgi-0.1.2.tar.gz", "has_sig": false, "md5_digest": "97dd28b1e5fd5b02612d37f5c905b475", "packagetype": "sdist", "python_version": "source", "requires_python": ">=3.6,<4.0", "size": 4723, "upload_time": "2019-06-13T12:42:22", "url": "https://files.pythonhosted.org/packages/3e/c7/29019321d54b18624859c597cd39b5fca40c12f3fd74122f473b79b10117/content_size_limit_asgi-0.1.2.tar.gz" } ] }