Repository: aws-powertools/powertools-lambda-python

Issue:
Feature request: Additional HTTP Error Code Exceptions
### Use case

ref: https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/event_handler/exceptions.py

I need to handle additional HTTP error codes, specifically 413 (Payload Too Large). I notice that the current exceptions.py only implements a subset of HTTP error codes (400, 401, 404, 500).
While I understand I can extend ServiceError to create my own custom exceptions, this creates inconsistency in my codebase where some errors come from PowerTools and others from my custom definitions.

Are there plans to expand the list of predefined exceptions to cover more HTTP status codes?
What is the rationale behind which HTTP errors are currently implemented vs. those that aren't?

### Solution/User Experience

Add status code

### Alternative solutions

```markdown

```

### Acknowledgment

- [x] This feature request meets [Powertools for AWS Lambda (Python) Tenets](https://docs.powertools.aws.dev/lambda/python/latest/#tenets)
- [x] Should this be considered in other Powertools for AWS Lambda languages? i.e. [Java](https://github.com/aws-powertools/powertools-lambda-java/), [TypeScript](https://github.com/aws-powertools/powertools-lambda-typescript/), and [.NET](https://github.com/aws-powertools/powertools-lambda-dotnet/)

Hints from issue comments:
@boring-cyborg: Thanks for opening your first issue here! We'll come back to you as soon as we can.
In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: [Invite link](https://discord.gg/B8zZKbbyET)
@anafalcao: Hello @kazu728 ! 
Thank you for opening this issue. You raise a valid point about the current subset of implemented HTTP status codes. The initial selection of HTTP error codes was based on project decisions made at that time, but I believe would be beneficial to incorporate 413. We could add the API calls [CommomErrors](https://docs.aws.amazon.com/lambda/latest/api/CommonErrors.html), meaning adding 403, 413, 408 e 503. Do you suggest others?

Would you like to submit a PR with these changes?@kazu728: Thank you for deaIing with this issue. I do not suggest any other changes.

> Would you like to submit a PR with these changes?

Yes, I will submit it.@leandrodamascena: Hi @kazu728! Please let me know if you have time to submit a PR in the next 2 weeks. I'm not pushing, but since we are planing the release, we'd like to add this.

I totally understand if you can't, we're all busy.@kazu728: Sorry, I will deal with this issue by the weekend.@leandrodamascena: Hey @kazu728! Thanks a lot.@github-actions: ### ⚠️COMMENT VISIBILITY WARNING⚠️
This issue is now closed. Please be mindful that future comments are hard for our team to see.

If you need more assistance, please either tag a [team member](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/MAINTAINERS.md#current-maintainers) or open a new issue that references this one.

If you wish to keep having a conversation with other community members under this issue feel free to do so.

Solution patch:
diff --git a/aws_lambda_powertools/event_handler/exceptions.py b/aws_lambda_powertools/event_handler/exceptions.py
index ca5dbbc98..e524d8a0e 100644
--- a/aws_lambda_powertools/event_handler/exceptions.py
+++ b/aws_lambda_powertools/event_handler/exceptions.py
@@ -2,7 +2,7 @@ from http import HTTPStatus
 
 
 class ServiceError(Exception):
-    """API Gateway and ALB HTTP Service Error"""
+    """Powertools class HTTP Service Error"""
 
     def __init__(self, status_code: int, msg: str):
         """
@@ -18,28 +18,56 @@ class ServiceError(Exception):
 
 
 class BadRequestError(ServiceError):
-    """API Gateway and ALB Bad Request Error (400)"""
+    """Powertools class Bad Request Error (400)"""
 
     def __init__(self, msg: str):
         super().__init__(HTTPStatus.BAD_REQUEST, msg)
 
 
 class UnauthorizedError(ServiceError):
-    """API Gateway and ALB Unauthorized Error (401)"""
+    """Powertools class Unauthorized Error (401)"""
 
     def __init__(self, msg: str):
         super().__init__(HTTPStatus.UNAUTHORIZED, msg)
 
 
+class ForbiddenError(ServiceError):
+    """Powertools class Forbidden Error (403)"""
+
+    def __init__(self, msg: str):
+        super().__init__(HTTPStatus.FORBIDDEN, msg)
+
+
 class NotFoundError(ServiceError):
-    """API Gateway and ALB Not Found Error (404)"""
+    """Powertools class Not Found Error (404)"""
 
     def __init__(self, msg: str = "Not found"):
         super().__init__(HTTPStatus.NOT_FOUND, msg)
 
 
+class RequestTimeoutError(ServiceError):
+    """Powertools class Request Timeout Error (408)"""
+
+    def __init__(self, msg: str):
+        super().__init__(HTTPStatus.REQUEST_TIMEOUT, msg)
+
+
+class RequestEntityTooLargeError(ServiceError):
+    """Powertools class Request Entity Too Large Error (413)"""
+
+    def __init__(self, msg: str):
+        super().__init__(HTTPStatus.REQUEST_ENTITY_TOO_LARGE, msg)
+
+
 class InternalServerError(ServiceError):
-    """API Gateway and ALB Internal Server Error (500)"""
+    """Powertools class Internal Server Error (500)"""
 
     def __init__(self, message: str):
         super().__init__(HTTPStatus.INTERNAL_SERVER_ERROR, message)
+
+
+class ServiceUnavailableError(ServiceError):
+    """Powertools class Service Unavailable Error (503)"""
+
+    def __init__(self, msg: str):
+        super().__init__(HTTPStatus.SERVICE_UNAVAILABLE, msg)
diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md
index 12a2a77b4..d392bf7ac 100644
--- a/docs/core/event_handler/api_gateway.md
+++ b/docs/core/event_handler/api_gateway.md
@@ -595,9 +595,9 @@ You can easily raise any HTTP Error back to the client using `ServiceError` exce
 ???+ info
     If you need to send custom headers, use [Response](#fine-grained-responses) class instead.
 
-We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500.
+We provide pre-defined errors for the most popular ones based on [AWS Lambda API Reference Common Erros](https://docs.aws.amazon.com/lambda/latest/api/CommonErrors.html).
 
-```python hl_lines="6-11 23 28 33 38 43" title="Raising common HTTP Status errors (4xx, 5xx)"
+```python hl_lines="7-15 27 32 37 42 47 52 57 62 67" title="Raising common HTTP Status errors (4xx, 5xx)"
 --8<-- "examples/event_handler_rest/src/raising_http_errors.py"
 ```
 
diff --git a/examples/event_handler_rest/src/raising_http_errors.py b/examples/event_handler_rest/src/raising_http_errors.py
index 97e7cc504..b62597eaf 100644
--- a/examples/event_handler_rest/src/raising_http_errors.py
+++ b/examples/event_handler_rest/src/raising_http_errors.py
@@ -5,9 +5,13 @@ from aws_lambda_powertools import Logger, Tracer
 from aws_lambda_powertools.event_handler import APIGatewayRestResolver
 from aws_lambda_powertools.event_handler.exceptions import (
     BadRequestError,
+    ForbiddenError,
     InternalServerError,
     NotFoundError,
+    RequestEntityTooLargeError,
+    RequestTimeoutError,
     ServiceError,
+    ServiceUnavailableError,
     UnauthorizedError,
 )
 from aws_lambda_powertools.logging import correlation_paths
@@ -28,21 +32,40 @@ def unauthorized_error():
     raise UnauthorizedError("Unauthorized")  # HTTP 401
 
 
+@app.get(rule="/forbidden-error")
+def forbidden_error():
+    raise ForbiddenError("Access denied")  # HTTP 403
+
+
 @app.get(rule="/not-found-error")
 def not_found_error():
     raise NotFoundError  # HTTP 404
 
 
+@app.get(rule="/request-timeout-error")
+def request_timeout_error():
+    raise RequestTimeoutError("Request timed out")  # HTTP 408
+
+
 @app.get(rule="/internal-server-error")
 def internal_server_error():
     raise InternalServerError("Internal server error")  # HTTP 500
 
 
+@app.get(rule="/request-entity-too-large-error")
+def request_entity_too_large_error():
+    raise RequestEntityTooLargeError("Request payload too large")  # HTTP 413
+
+
 @app.get(rule="/service-error", cors=True)
 def service_error():
     raise ServiceError(502, "Something went wrong!")
 
 
+@app.get(rule="/service-unavailable-error")
+def service_unavailable_error():
+    raise ServiceUnavailableError("Service is temporarily unavailable")  # HTTP 503
+
 @app.get("/todos")
 @tracer.capture_method
 def get_todos():


Test patch:
diff --git a/tests/functional/event_handler/required_dependencies/test_api_gateway.py b/tests/functional/event_handler/required_dependencies/test_api_gateway.py
index fdab6080f..e448d335d 100644
--- a/tests/functional/event_handler/required_dependencies/test_api_gateway.py
+++ b/tests/functional/event_handler/required_dependencies/test_api_gateway.py
@@ -26,9 +26,13 @@ from aws_lambda_powertools.event_handler.api_gateway import (
 )
 from aws_lambda_powertools.event_handler.exceptions import (
     BadRequestError,
+    ForbiddenError,
     InternalServerError,
     NotFoundError,
+    RequestEntityTooLargeError,
+    RequestTimeoutError,
     ServiceError,
+    ServiceUnavailableError,
     UnauthorizedError,
 )
 from aws_lambda_powertools.shared import constants
@@ -873,6 +877,21 @@ def test_service_error_responses(json_dump):
     expected = {"statusCode": 401, "message": "Unauthorized"}
     assert result["body"] == json_dump(expected)
 
+    # GIVEN a ForbiddenError
+    @app.get(rule="/forbidden-error", cors=False)
+    def forbidden_error():
+        raise ForbiddenError("Access denied")
+
+    # WHEN calling the handler
+    # AND path is /forbidden-error
+    result = app({"path": "/forbidden-error", "httpMethod": "GET"}, None)
+    # THEN return the forbidden error response
+    # AND status code equals 403
+    assert result["statusCode"] == 403
+    assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+    expected = {"statusCode": 403, "message": "Access denied"}
+    assert result["body"] == json_dump(expected)
+
     # GIVEN an NotFoundError
     @app.get(rule="/not-found-error", cors=False)
     def not_found_error():
@@ -888,6 +907,36 @@ def test_service_error_responses(json_dump):
     expected = {"statusCode": 404, "message": "Not found"}
     assert result["body"] == json_dump(expected)
 
+    # GIVEN a RequestTimeoutError
+    @app.get(rule="/request-timeout-error", cors=False)
+    def request_timeout_error():
+        raise RequestTimeoutError("Request timed out")
+
+    # WHEN calling the handler
+    # AND path is /request-timeout-error
+    result = app({"path": "/request-timeout-error", "httpMethod": "GET"}, None)
+    # THEN return the request timeout error response
+    # AND status code equals 408
+    assert result["statusCode"] == 408
+    assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+    expected = {"statusCode": 408, "message": "Request timed out"}
+    assert result["body"] == json_dump(expected)
+
+    # GIVEN a RequestEntityTooLargeError
+    @app.get(rule="/request-entity-too-large-error", cors=False)
+    def request_entity_too_large_error():
+        raise RequestEntityTooLargeError("Request payload too large")
+
+    # WHEN calling the handler
+    # AND path is /request-entity-too-large-error
+    result = app({"path": "/request-entity-too-large-error", "httpMethod": "GET"}, None)
+    # THEN return the request entity too large error response
+    # AND status code equals 413
+    assert result["statusCode"] == 413
+    assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+    expected = {"statusCode": 413, "message": "Request payload too large"}
+    assert result["body"] == json_dump(expected)
+
     # GIVEN an InternalServerError
     @app.get(rule="/internal-server-error", cors=False)
     def internal_server_error():
@@ -903,6 +952,21 @@ def test_service_error_responses(json_dump):
     expected = {"statusCode": 500, "message": "Internal server error"}
     assert result["body"] == json_dump(expected)
 
+    # GIVEN a ServiceUnavailableError
+    @app.get(rule="/service-unavailable-error", cors=False)
+    def service_unavailable_error():
+        raise ServiceUnavailableError("Service is temporarily unavailable")
+
+    # WHEN calling the handler
+    # AND path is /service-unavailable-error
+    result = app({"path": "/service-unavailable-error", "httpMethod": "GET"}, None)
+    # THEN return the service unavailable error response
+    # AND status code equals 503
+    assert result["statusCode"] == 503
+    assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
+    expected = {"statusCode": 503, "message": "Service is temporarily unavailable"}
+    assert result["body"] == json_dump(expected)
+
     # GIVEN an ServiceError with a custom status code
     @app.get(rule="/service-error")
     def service_error():