#1 mORMot 2 » Possible mORMot2 bug: TServiceCustomAnswer.Header contains full HTTP » 2026-10-10 01:26:49

Alfonso
Replies: 1

Hi, I had this problem and made the post using IA. I fixed it overwritting the headers in the service caller but I don't know if it is a bug or not.

Thanks in advance,


# Possible mORMot2 bug: TServiceCustomAnswer.Header contains full HTTP response headers when proxying SOA services

**Environment**
- mORMot2 2.4
- Delphi 12
- HTTP server using `TRestHttpServer`
- SOA interfaces using `TServiceCustomAnswer`
- SOA client using `TRestHttpClientWinHttp`

## Problem description

I have two mORMot2 REST servers:

1. **Magnus Server**, which exposes an SOA interface returning `TServiceCustomAnswer`, used to retrieve PDF documents.
2. **B2B Server**, which acts as a proxy, consuming the Magnus SOA interface through `TRestHttpClientWinHttp` and returning the same `TServiceCustomAnswer` to its clients.

Everything works correctly when the remote service returns HTTP 200 with a PDF.

However, when the remote service returns HTTP 404 with a JSON error, the proxy server unexpectedly returns:

```json
{
  "errorCode": 500,
  "errorText": "Unsafe HTTP header rejected"
}
```

## Original service implementation

The Magnus service returns a PDF or raises an exception if the file does not exist:

```pascal
if not FileExists(FName) then raise EExNotFound.Create('FA No encontrada');

Result.Content := StringFromFile(FName);
Result.Header := 'Content-Type: application/pdf'#13#10 + 'Content-Disposition: inline; filename="' + StringToUtf8(ExtractFileName(FName)) + '"';
Result.Status := 200;
```

The exception handler correctly converts `EExNotFound` into HTTP 404 with this JSON response:

```json
{
  "errorCode": 404,
  "error": {
    "code": "NOT_FOUND",
    "message": "FA No encontrada"
  }
}
```

The remote Magnus server behaves correctly.

## Problem in the proxy

The B2B server invokes the remote SOA method using `TRestHttpClientWinHttp`.

The client receives the expected response:

```text
POST MagnusServiceLocalB2B.FactPdf status=404
```

But when B2B attempts to return the resulting `TServiceCustomAnswer`, it fails with HTTP 500.

Using the debugger, I inspected the B2B server's `Call` context:

**Call.OutStatus:**

```text
404
```

**Call.OutBody:**

```json
{
  "errorCode": 404,
  "error": {
    "code": "NOT_FOUND",
    "message": "FA No encontrada"
  }
}
```

**Call.OutHead:**

```text
HTTP/1.1 404 Not Found\r\n
Content-Length: 83\r\n
Content-Type: application/json\r\n
Server: mORMot2 (Win)\r\n
X-Powered-By: mORMot 2.4\r\n
\r\n
```

The issue is that `Call.OutHead` contains the **complete HTTP response header block**, including the HTTP status line and the final empty line.

This causes `IsInvalidHttpHeader(Call.OutHead)` to reject the headers.

## Relevant mORMot2 source code

In `mormot.soa.client.pas`, inside `TServiceFactoryClient.InternalInvoke`, the client copies the received headers into the custom answer:

```pascal
aServiceCustomAnswer^.Status := status;
aServiceCustomAnswer^.Header := head;
aServiceCustomAnswer^.Content := resp;
```

When the SOA server subsequently processes this `TServiceCustomAnswer`, the header is assigned to the outgoing HTTP headers.

In `mormot.rest.server.pas`, the following validation fails:

```pascal
if (Call.OutHead <> '') and
   not (rsoHttpHeaderCheckDisable in fOptions) and
   IsInvalidHttpHeader(Call.OutHead) then
  ctxt.Error('Unsafe HTTP header rejected', HTTP_SERVERERROR);
```

This suggests a mismatch between the HTTP headers returned by the SOA client and the format expected by the SOA server when forwarding `TServiceCustomAnswer`.

## Working workaround

I resolved the issue by explicitly replacing the headers in the B2B proxy after the remote call:

```pascal
if Result.Status = 200 then
  Result.Header := 'Content-Type: application/pdf'#13#10 + 'Content-Disposition: inline; filename="' + IdDoc.ToString + '.PDF"'
else
  Result.Header := 'Content-Type: application/json';
```

After this modification:

- HTTP 200 responses continue to return the PDF correctly.
- HTTP 404 responses preserve the original status code.
- The original JSON error body is preserved.
- The proxy no longer returns HTTP 500.
- No modification to the original Magnus service or its exception handler is necessary.

## Question for the mORMot2 developers

Is this the intended behavior of `TServiceCustomAnswer.Header` when returned by `TRestHttpClientWinHttp`?

Should `TServiceFactoryClient.InternalInvoke` normalize the received HTTP headers before assigning them to `aServiceCustomAnswer^.Header`, removing the HTTP status line and the terminating empty line?

Alternatively, should the SOA server normalize these headers before assigning them to `Call.OutHead`?

It seems that directly forwarding a `TServiceCustomAnswer` between two mORMot2 SOA services can produce an invalid HTTP response, particularly when the upstream service returns a non-200 status.

Would it make sense to address this normalization in mORMot2 itself?

Thank you.

Board footer

Powered by FluxBB