#1 2026-10-10 01:26:49

Alfonso
Member
Registered: 2026-10-10
Posts: 1

Possible mORMot2 bug: TServiceCustomAnswer.Header contains full HTTP

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.

Offline

#2 2026-10-10 08:03:42

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 15,611
Website

Re: Possible mORMot2 bug: TServiceCustomAnswer.Header contains full HTTP

Welcome here! smile

Please follow the forum BB format, not markdown - or use rather plain text.
All this is a bit confusing, you should have asked the IA to make it clear and short, not verbose and all.
Also ask it to find the root cause, it would have found better than just rewriting the header in the middle B2B server. See the latest pull requests on github.

Check
https://github.com/synopse/mORMot2/commit/e46374f34

Note that the THttpClientSocket is to be preferred even on Windows, because it is faster, and more proven against complex redirection, proxies and such.
With the latest weeks, we improved SChannel support so I see no reason to not use it instead of TWinHttp.

Offline

Board footer

Powered by FluxBB