Metadata-Version: 2.1
Name: HttpMessageProcessor
Version: 3.1.0
Author-email: Eduardo Almeida <eduardo.almeida@e-deploy.com.br>
Maintainer-email: Eduardo Almeida <eduardo.almeida@e-deploy.com.br>, Max Guenes <max.santos@e-deploy.com.br>
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: MessageProcessor<6,>=4.1
Requires-Dist: SimpleHttpRouter~=1.0
Requires-Dist: iso8601==2.1.0; python_version >= "3.8" and python_version < "4.0"
Requires-Dist: nh3<0.4.0,>=0.3.3

# HttpMessageProcessor

HTTP routing and request-parsing library built on top of the `MessageProcessor` and `SimpleHttpRouter` packages. It provides two packages: a core routing layer (`httpmessageprocessor`) and a higher-level automation layer (`httpmessageprocessorutil`).

## Installation

```bash
pip install HttpMessageProcessor --extra-index-url https://pip.e-deploy.com.br
```

## Packages

### `httpmessageprocessor` — Core routing

Routes `HttpRequestMessage` events to per-route `MessageProcessor` instances.

```python
from httpmessageprocessor import (
    HttpMessageProcessor,
    HttpMessageProcessorMessageHandler,
    ApiRequest,
    RouterProcessorBuilder,
)
```

**`HttpMessageProcessor`** accepts either a `Dict[Route, MessageProcessor]` mapping or a custom `RouterProcessorBuilder`.

**`HttpMessageProcessorMessageHandler`** is a convenience wrapper that binds `HttpMessageProcessor` to `DefaultToken.TK_HTTP_MSG` on the message bus.

### `httpmessageprocessorutil` — Automation layer

#### `AutoMappedMessageProcessor`

Automates the full HTTP request lifecycle: parse → business call → serialize response.

```python
from httpmessageprocessorutil.automapped import AutoMappedMessageProcessor

processor = AutoMappedMessageProcessor(
    request_type=MyRequestDto,   # DTO class (or None for no body)
    interactor=my_interactor,    # must have execute(dto) or execute()
    exception_map={              # optional: maps exception types to HTTP status codes
        NotFoundException: 404,
        ConflictException: (409, b"conflict"),
        MyException: lambda e: HttpResponseMessage(422, ...),
    },
    success_status=200,          # optional, default 200
    extra_mapping=None,          # optional, see below
    response_content_type=None,  # optional, forces Content-Type header
    sanitizer=...,               # optional, see Sanitization section
)
```

The interactor must expose exactly one `execute` method accepting zero or one parameter. When `request_type` is `None`, `execute()` is called with no arguments.

#### DTO parsing (`ApiRequestParser`)

The parser deserializes `ApiRequest` into a typed DTO using Python type annotations. It searches **path parameters → query parameters → JSON body** in that priority order and automatically resolves field names across `snake_case`, `camelCase`, `PascalCase`, and `kebab-case` variants.

**Supported field types:** `int`, `float`, `bool`, `str`, `Decimal`, `date`, `datetime`, `UUID`, `Enum`, `List[T]`, `Dict[K, V]`, `Optional[T]`, `Union[A, B]`, nested DTOs, `ForwardRef`.

**GET requests:** the body is never parsed; all values come from path/query parameters.

#### `extra_mapping`

Pass as the `extra_mapping` keyword argument to `AutoMappedMessageProcessor` or `ApiRequestParser`:

```python
extra_mapping = {
    # Remove attributes absent from the payload (useful for PATCH)
    "removeNotSent": True,

    # Per-field overrides
    "user_id": {
        "location": "path",   # "path" | "query" | "header" | "body"
        "name": "userId",     # key name to look up
        "type": UUID,         # override parsed type
        "sanitize": False,    # skip sanitization for this field
    },
}
```

#### `@auto_map` decorator

Override per-field parsing metadata directly on the DTO class:

```python
from httpmessageprocessorutil.automapped import auto_map

@auto_map({
    "host": {"location": "header", "name": "X-Host", "type": str},
})
class MyRequestDto:
    def __init__(self, host: str, name: str):
        self.host = host
        self.name = name
```

#### `@exec_map` decorator

Attach HTTP metadata to custom exception classes:

```python
from httpmessageprocessorutil.automapped import exec_map

@exec_map({"status_code": 422, "body": {"error": "unprocessable"}})
class MyException(Exception):
    pass
```

#### Exception formatting

`PropertyError` subclasses are always returned as **400** with a structured body (unless explicitly mapped):

```json
{"property": "field_name", "reason": "cannot be null"}
```

For list errors, an `innerError` key is included with the nested cause.

The `exception_map` values can be:
- `int` — status code; body is auto-serialised from the exception if it has a custom `__init__`
- `(int, body)` — explicit status code and body (`bytes` or `dict`)
- `callable` — receives the exception, returns `HttpResponseMessage` or a JSON-serialisable object

#### Global exception map

Use `GlobalExceptionMap` to register application-wide exception mappings once, instead of repeating them in every processor's `exception_map`:

```python
from httpmessageprocessorutil.automapped import GlobalExceptionMap

# At application startup
GlobalExceptionMap.set({
    ForbiddenException: 403,
    UnauthorizedException: 401,
    NotFoundException: 404,
})
```

`ExceptionFormatter` checks the per-instance `exception_map` first, then falls back to the global map. Per-instance entries always take priority. Changes to the global map take effect immediately on all processors.

### Sanitization

By default, all `str` values parsed from requests **and** responses are sanitized using `nh3` (strips all HTML tags). This prevents XSS from reaching business logic or clients.

Plain-text characters and entities round-trip unchanged: bare `<`, `>`, `&`, sequences like `->` or `AT&T`, and pre-existing HTML entities (`&gt;`, `&amp;`, `&#60;`, `&#x3C;`) are preserved verbatim — only actual tags and attributes are stripped.

```python
from httpmessageprocessorutil.sanitizer import Sanitizer, Nh3Sanitizer, SanitizeMode

# Disable sanitization entirely
processor = AutoMappedMessageProcessor(..., sanitizer=None)

# Sanitize only requests (preserve HTML in responses)
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.REQUEST_ONLY)

# Sanitize only responses
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.RESPONSE_ONLY)

# Disable via mode (equivalent to sanitizer=None)
processor = AutoMappedMessageProcessor(..., sanitize_mode=SanitizeMode.NONE)

# Custom sanitizer
class MySanitizer(Sanitizer):
    def _types_to_sanitize(self):
        return [str]

    def _sanitize(self, value):
        return my_custom_clean(value)

processor = AutoMappedMessageProcessor(..., sanitizer=MySanitizer())

# Custom sanitizer with different logic per direction
class DirectionalSanitizer(Sanitizer):
    def _types_to_sanitize(self):
        return [str]

    def _sanitize(self, value):
        return value

    def sanitize_request(self, value):
        # strict cleaning for incoming data
        return strict_clean(value)

    def sanitize_response(self, value):
        # allow some HTML in outgoing data
        return allow_safe_html(value)
```

The `Sanitizer` base class provides `sanitize_request(value)` and `sanitize_response(value)` methods that default to calling `sanitize(value)`. Override them independently to apply different sanitization logic for requests vs responses.

#### Exempting specific fields

Some business fields must keep characters that the HTML tokenizer reads as markup. A `<` glued to a letter opens a tag, so `"LANCHE>TESTE<COZINHA"` is truncated to `"LANCHE>TESTE"` — `<COZINHA` is indistinguishable from `<script`, so the sanitizer cannot preserve it without reopening the XSS hole it exists to close. Declare such fields exempt instead.

The `@no_sanitize` decorator marks fields on the class itself, so it applies to both requests and responses:

```python
from httpmessageprocessorutil.automapped import no_sanitize

@no_sanitize("product_name", "notes")
class ProductRequestDto:
    def __init__(self, product_name: str, notes: str, description: str):
        ...   # product_name and notes keep raw values; description is sanitized
```

`extra_mapping` offers the same control for requests only, at the **root level** of the mapping:

```python
extra_mapping = {
    "product_name": {"sanitize": False},
}
```

Rules:

- **The most local declaration wins.** `extra_mapping={"product_name": {"sanitize": True}}` re-enables sanitization for a field the decorator exempted.
- **Exemption is per class.** Attribute metadata is resolved once per class, so if the same class is used by more than one field, an exemption declared for it applies at every usage site. This is why the `sanitize` key is rejected in a nested `extra_mapping` scope — a nested declaration would leak to sibling fields of the same class, or be dropped, depending on the order of the parent DTO's `__init__` parameters. `ApiRequestParser` raises `NestedSanitizeMappingNotSupported` at construction time instead of guessing.
- **No cascade into nested DTOs.** Exempting a field whose type is another DTO (or `List[SomeDto]`) does not exempt that DTO's own fields — declare `@no_sanitize` on the nested class instead. This keeps an exemption from silently disabling sanitization across an arbitrarily deep subtree.
- **Containers of scalars on the exempt field do inherit** it: `List[str]`, `Dict[str, str]` and `Optional[str]` fields keep their raw values.
- Field names are matched against the **python attribute name**, but the two sides read that name from different places: the request side matches the `__init__` parameter names, the response side matches the `obj.__dict__` keys. A DTO that renames on assignment (`self.product_name = name`) therefore needs both names declared to be exempt end to end.
- On the response side only direct `str` attributes are sanitized to begin with; strings nested inside `list` or `dict` attributes are already never sanitized, so exempting those is a no-op.
- An exempt field returns whatever arrived, including an active XSS payload. That is the cost of the feature — exempt only fields whose business rule demands it.

#### Changing the default sanitizer

Use `set_default_sanitizer()` to change the sanitizer used by all future instances:

```python
# Use a custom sanitizer by default
AutoMappedMessageProcessor.set_default_sanitizer(MySanitizer())

# Disable sanitization by default
AutoMappedMessageProcessor.set_default_sanitizer(None)
```

Passing an explicit `sanitizer=` to the constructor always overrides the class default. `sanitizer=None` disables sanitization regardless of `sanitize_mode`.

## Data flow

```
MessageBus (HttpRequestMessage)
  → HttpMessageProcessorMessageHandler
    → HttpMessageProcessor          # route dispatch
      → AutoMappedMessageProcessor.parse_data()
          → ApiRequestParser.parse()   # builds typed DTO; sanitizes strings
      → AutoMappedMessageProcessor.call_business()
          → interactor.execute(dto)
      → AutoMappedMessageProcessor.format_response() / format_exception()
  → MessageBus (HttpResponseMessage)
```

## Response serialization

- Default: JSON (`application/json`), status 200 (or `success_status`).
- `Decimal` values are serialised as `float` rounded to 8 decimal places.
- `Enum` values are serialised by their `.value`.
- Pass `response_content_type` to force a custom `Content-Type` and skip JSON serialisation.

## Requirements

- Python ≥ 3.8
- `MessageProcessor ~= 4.1`
- `SimpleHttpRouter ~= 1.0`
- `iso8601 == 2.1.0`
- `nh3 >= 0.3.3, < 0.4.0`
