{"id":16339880,"url":"https://github.com/yuvalherziger/aiohttp-catcher","last_synced_at":"2025-03-22T23:33:03.404Z","repository":{"id":45115158,"uuid":"442748915","full_name":"yuvalherziger/aiohttp-catcher","owner":"yuvalherziger","description":"A centralized error handler for aiohttp servers","archived":false,"fork":false,"pushed_at":"2022-10-13T05:00:54.000Z","size":348,"stargazers_count":6,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-18T15:48:59.795Z","etag":null,"topics":["aiohttp","aiohttp-server","asyncio","error-handling","python"],"latest_commit_sha":null,"homepage":"","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/yuvalherziger.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2021-12-29T11:17:01.000Z","updated_at":"2024-10-05T07:19:59.000Z","dependencies_parsed_at":"2022-09-12T02:11:56.807Z","dependency_job_id":null,"html_url":"https://github.com/yuvalherziger/aiohttp-catcher","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuvalherziger%2Faiohttp-catcher","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuvalherziger%2Faiohttp-catcher/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuvalherziger%2Faiohttp-catcher/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yuvalherziger%2Faiohttp-catcher/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yuvalherziger","download_url":"https://codeload.github.com/yuvalherziger/aiohttp-catcher/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245036127,"owners_count":20550662,"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":["aiohttp","aiohttp-server","asyncio","error-handling","python"],"created_at":"2024-10-10T23:55:24.246Z","updated_at":"2025-03-22T23:33:03.045Z","avatar_url":"https://github.com/yuvalherziger.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aiohttp-catcher\n\n\u003cdiv align=\"left\"\u003e\n  \u003ca href=\"https://github.com/yuvalherziger/aiohttp-catcher/actions?query=workflow%3ACI\"\u003e\u003cimg alt=\"CI Job\"  height=\"20\" src=\"https://github.com/yuvalherziger/aiohttp-catcher/workflows/CI/badge.svg\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pypi.org/project/aiohttp-catcher\"\u003e\u003cimg src=\"https://badge.fury.io/py/aiohttp-catcher.svg\" alt=\"PyPI version\" height=\"20\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/yuvalherziger/aiohttp-catcher/actions\"\u003e\u003cimg src=\"https://gist.githubusercontent.com/yuvalherziger/14417a5617e959df89a524f327f86c92/raw/395fe027d00c73a612a117cc81c882d9ad012abc/aiohttp-catcher-cov.svg\" height=\"20\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/yuvalherziger/aiohttp-catcher/blob/main/LiCENSE\"\u003e\u003cimg src=\"https://img.shields.io/pypi/l/aiohttp-catcher?color=%233383FF\" height=\"20\"\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/yuvalherziger/aiohttp-catcher\"\u003e\u003cimg src=\"https://img.shields.io/pypi/dm/aiohttp-catcher?color=%23784212\" height=\"20\"\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n***\n\naiohttp-catcher is a centralized error handler for [aiohttp servers](https://docs.aiohttp.org/en/stable/web.html).\nIt enables consistent error handling across your web server or API, so your code can raise Python exceptions that\nwill be mapped to consistent, user-friendly error messages.\n\n***\n\n- [Quickstart](#quickstart)\n- [What's New in 0.3.0?](#what-s-new-in-030-)\n- [Key Features](#key-features)\n  * [Return a Constant](#return-a-constant)\n  * [Stringify the Exception](#stringify-the-exception)\n  * [Canned HTTP 4xx and 5xx Errors (aiohttp Exceptions)](#canned-http-4xx-and-5xx-errors--aiohttp-exceptions-)\n  * [Callables and Awaitables](#callables-and-awaitables)\n  * [Handle Several Exceptions Similarly](#handle-several-exceptions-similarly)\n  * [Scenarios as Dictionaries](#scenarios-as-dictionaries)\n  * [Additional Fields](#additional-fields)\n  * [Default for Unhandled Exceptions](#default-for-unhandled-exceptions)\n- [Development](#development)\n\n***\n\nTL;DR:\n\n![aiohttp-catcher-diagram](img/aiohttp-catcher-diagram.png)\n\n***\n\n## Quickstart\n\nInstall aiohttp-catcher:\n\n```shell\npip install aiohttp-catcher\n```\n\nStart catching errors in your aiohttp-based web server:\n\n```python\nfrom aiohttp import web\nfrom aiohttp_catcher import catch, Catcher\n\nasync def divide(request):\n  quotient = 1 / 0\n  return web.Response(text=f\"1 / 0 = {quotient}\")\n\n\nasync def main():\n  # Add a catcher:\n  catcher = Catcher()\n\n  # Register error-handling scenarios:\n  await catcher.add_scenario(\n    catch(ZeroDivisionError).with_status_code(400).and_return(\"Zero division makes zero sense\")\n  )\n\n  # Register your catcher as an aiohttp middleware:\n  app = web.Application(middlewares=[catcher.middleware])\n  app.add_routes([web.get(\"/divide-by-zero\", divide)])\n  web.run_app(app)\n```\n\nMaking a request to `/divide-by-zero` will return a 400 status code with the following body:\n```json\n{\"code\": 400, \"message\": \"Zero division makes zero sense\"}\n```\n\n\u003cdiv\u003e\n\u003ch3\u003e\n  \u003cspan style=\"color:#ff6500\"\u003eIMPORTANT NOTE:\u003c/span\u003e\n  \u003cspan\u003eaiohttp's order of middleware matters\u003c/span\u003e\n\u003c/h3\u003e\n\u003c/div\u003e\n\nMiddlewares that are appended further in the list of your app's middlewares act\nearlier. Consider the following example:\n```python\napp = web.Application(middlewares=[middleware1, middleware2])\n```\n\nIn the above case, `middleware2` will be triggered first, and only then\nwill `middleware1` be triggered.  This means two things:\n\n1. If you register another middleware that catches exceptions but doesn't raise them\n   when it's done, you will need to add it **before** your _aiohttp-catcher_ middleware\n   or the other middleware will shadow _aiohttp-catcher_.\n2. If you register another middleware that relies on exceptions being raised, you want\n   to make sure it's added **after** your _aiohttp-catcher_ middleware, to avoid having\n   your _aiohttp-catcher_ middleware shadow the other middleware. One good example is\n   [aiohttp-debugtoolbar](https://github.com/aio-libs/aiohttp-debugtoolbar), which, like\n   _aiohttp-catcher_, expects exceptions to be thrown and raises them when its middleware's\n   execution is done. In this case, you want to set up _aiohttp-debugtoolbar_ after appending\n   your _aiohttp-catcher_ middleware.\n\n***\n\n## What's New in 0.3.0?\n\n* **Canned Scenarios:** You can now use a [canned list of scenarios](#canned-http-4xx-and-5xx-errors--aiohttp-exceptions-),\n  capturing all of [aiohttp's web exceptions](https://docs.aiohttp.org/en/latest/web_exceptions.html) out of the box.\n* **More flexible Callables and Awaitables:** Callables and Awaitables are now invoked with a second argument,  \n  the aiohttp `Request` instance, to add more flexibility to custom messages.\n\n***\n\n## Key Features\n\n### Return a Constant\n\nIn case you want some exceptions to return a constant message across your application, you can do\nso by using the `and_return(\"some value\")` method:\n\n```python\nawait catcher.add_scenario(\n  catch(ZeroDivisionError).with_status_code(400).and_return(\"Zero division makes zero sense\")\n)\n```\n\n***\n\n### Stringify the Exception\n\nIn some cases, you would want to return a stringified version of your exception, should it entail\nuser-friendly information.\n\n```python\nclass EntityNotFound(Exception):\n  def __init__(self, entity_id, *args, **kwargs):\n    super(EntityNotFound, self).__init__(*args, **kwargs)\n    self.entity_id = entity_id\n\n  def __str__(self):\n    return f\"Entity {self.entity_id} could not be found\"\n\n\n@routes.get(\"/user/{user_id}\")\nasync def get_user(request):\n  user_id = request.match_info.get(\"user_id\")\n  if user_id not in user_db:\n    raise EntityNotFound(entity_id=user_id)\n  return user_db[user_id]\n\n# Your catcher can be directed to stringify particular exceptions:\n\nawait catcher.add_scenario(\n  catch(EntityNotFound).with_status_code(404).and_stringify()\n)\n```\n\n***\n\n### Canned HTTP 4xx and 5xx Errors (aiohttp Exceptions)\n\nAs of version [0.3.0](https://github.com/yuvalherziger/aiohttp-catcher/releases/tag/0.3.0), you\ncan register [all of aiohttp's web exceptions](https://docs.aiohttp.org/en/latest/web_exceptions.html).\nThis is particularly useful when you want to ensure all possible HTTP errors are handled consistently.\n\nRegister the canned HTTP errors in the following way:\n\n```python\nfrom aiohttp import web\nfrom aiohttp_catcher import Catcher\nfrom aiohttp_catcher.canned import AIOHTTP_SCENARIOS\n\n\nasync def main():\n  # Add a catcher:\n  catcher = Catcher()\n  # Register aiohttp web errors:\n  await catcher.add_scenario(*AIOHTTP_SCENARIOS)\n  # Register your catcher as an aiohttp middleware:\n  app = web.Application(middlewares=[catcher.middleware])\n  web.run_app(app)\n```\n\nOnce you've registered the canned errors, you can rely on aiohttp-catcher to convert errors raised by aiohttp\nto user-friendly error messages.  For example, `curl`ing a non-existent route in your server will return the\nfollowing error out of the box:\n\n```json\n{\"code\": 404, \"message\": \"HTTPNotFound\"}\n```\n\n***\n\n### Callables and Awaitables\n\nIn some cases, you'd want the message returned by your server for some exceptions to call a custom\nfunction.  This function can either be a synchronous function or an awaitable one.  Your function should expect\ntwo arguments:\n\n1. The exception being raised by handlers.\n2. The request object - an instance of `aiohttp.web.Request`.\n\n```python\nfrom aiohttp.web import Request\nfrom aiohttp_catcher import catch, Catcher\n\n# Can be a synchronous function:\nasync def write_message(exc: Exception, request: Request):\n  return \"Whoops\"\n\ncatcher = Catcher()\nawait catcher.add_scenarios(\n  catch(MyCustomException2).with_status_code(401).and_call(write_message),\n  catch(MyCustomException2).with_status_code(403).and_call(lambda exc: str(exc))\n)\n\n```\n\n***\n\n### Handle Several Exceptions Similarly\n\nYou can handle several exceptions in the same manner by adding them to the same scenario:\n\n```python\nawait catcher.add_scenario(\n  catch(\n    MyCustomException1,\n    MyCustomException2,\n    MyCustomException3\n  ).with_status_code(418).and_return(\"User-friendly error message\")\n)\n```\n\n***\n\n### Scenarios as Dictionaries\n\nYou can register your scenarios as dictionaries as well:\n\n```python\nawait catcher.add_scenarios(\n  {\n    \"exceptions\": [ZeroDivisionError],\n    \"constant\": \"Zero division makes zero sense\",\n    \"status_code\": 400,\n  },\n  {\n    \"exceptions\": [EntityNotFound],\n    \"stringify_exception\": True,\n    \"status_code\": 404,\n  },\n  {\n    \"exceptions\": [IndexError],\n    \"func\": lambda exc: f\"Out of bound: {str(exc)}\",\n    \"status_code\": 418,\n  },\n)\n```\n\n***\n\n### Additional Fields\n\nYou can enrich your error responses with additional fields. You can provide additional fields using\nliteral dictionaries or with callables.  Your function should expect two arguments:\n\n1. The exception being raised by handlers.\n2. The request object - an instance of `aiohttp.web.Request`.\n\n```python\n# Using a literal dictionary:\nawait catcher.add_scenario(\n  catch(EntityNotFound).with_status_code(404).and_stringify().with_additional_fields({\"error_code\": \"ENTITY_NOT_FOUND\"})\n)\n\n# Using a function (or an async function):\nawait catcher.add_scenario(\n  catch(EntityNotFound).with_status_code(404).and_stringify().with_additional_fields(\n    lambda exc, req: {\"error_code\": e.error_code, \"method\": req.method}\n  )\n)\n```\n\n***\n\n### Default for Unhandled Exceptions\n\nExceptions that aren't registered with scenarios in your `Catcher` will default to 500, with a payload similar to\nthe following:\n\n```json\n{\"code\": 500, \"message\": \"Internal server error\"}\n```\n\n***\n\n## Development\n\nContributions are warmly welcomed.  Before submitting your PR, please run the tests using the following Make target:\n\n```bash\nmake ci\n```\n\nAlternatively, you can run each test separately:\n\nUnit tests:\n\n```bash\nmake test/py\n```\n\nLinting with pylint:\n\n```bash\nmake pylint\n```\n\nStatic security checks with bandit:\n\n```bash\nmake pybandit\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuvalherziger%2Faiohttp-catcher","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyuvalherziger%2Faiohttp-catcher","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyuvalherziger%2Faiohttp-catcher/lists"}