Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main

jobs:
build-and-test:
name: Build & Unit Tests (Java 21)
runs-on: ubuntu-latest

steps:
- name: Checkout source
uses: actions/checkout@v4

- name: Set up Java 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven

- name: Build and run unit tests
run: mvn --batch-mode --no-transfer-progress verify

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: '**/target/surefire-reports/*.xml'
retention-days: 7
125 changes: 125 additions & 0 deletions guides/APPENDIX-III.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,131 @@ the parameters where parameters.query, body and cookies are optional.
| body | HTTP request body for PUT, POST and PATCH | {"hello": "world"} |
| cookies | Cookie key-value | cookies.session-id=12345 |

## Calling a public endpoint example

The following pair of flows demonstrates calling an external public echo endpoint using GET and POST.
They can serve as a starting template for any outbound HTTP integration.

**GET example** — forwards an optional `message` query parameter and returns the echo response:

```yaml
flow:
id: 'ping-external'
description: 'Ping a public echo endpoint via AsyncHttpClient and return the response'
ttl: 15s
exception: 'ping.exception'

first.task: 'ping.call'

tasks:
- name: 'ping.call'
input:
- 'text(https://postman-echo.com) -> host'
- 'text(/get) -> url'
- 'text(GET) -> method'
- 'text(application/json) -> headers.accept'
- 'input.query_parameter.message -> parameters.query.message'
process: 'async.http.request'
output:
- 'text(application/json) -> output.header.content-type'
- 'result -> output.body'
description: 'GET https://postman-echo.com/get and return the echo response'
execution: end

- name: 'ping.exception'
input:
- 'error.code -> status'
- 'error.message -> message'
process: 'v1.hello.exception'
output:
- 'result.status -> output.status'
- 'result -> output.body'
description: 'Return error details to the caller'
execution: end
```

**POST example** — forwards the caller's request body and returns the echo response:

```yaml
flow:
id: 'ping-external-post'
description: 'POST a JSON body to a public echo endpoint via AsyncHttpClient and return the response'
ttl: 15s
exception: 'ping.post.exception'

first.task: 'ping.post.call'

tasks:
- name: 'ping.post.call'
input:
- 'text(https://postman-echo.com) -> host'
- 'text(/post) -> url'
- 'text(POST) -> method'
- 'text(application/json) -> headers.content-type'
- 'text(application/json) -> headers.accept'
- 'input.body -> body'
process: 'async.http.request'
output:
- 'text(application/json) -> output.header.content-type'
- 'result -> output.body'
description: 'POST request body to https://postman-echo.com/post and return the echo response'
execution: end

- name: 'ping.post.exception'
input:
- 'error.code -> status'
- 'error.message -> message'
process: 'v1.hello.exception'
output:
- 'result.status -> output.status'
- 'result -> output.body'
description: 'Return error details to the caller'
execution: end
```

Note that `host` must be the origin only (scheme + domain, no path), and `url` is the path portion.
Query parameters can be forwarded from the caller via `input.query_parameter.<name>` or set as
constants using `text(value) -> parameters.query.<name>`.

## HTTP trace logging

`AsyncHttpClient` emits DEBUG-level log entries for every outbound HTTP request and its corresponding
response. Each direction is a single, complete log entry — the request is logged before the call is
made and the response after the full body has been received and assembled.

A request log entry looks like:

```
DEBUG AsyncHttpClient -
>>> POST https://postman-echo.com/post
content-type: application/json
accept: application/json
body: {"message":"hello","from":"curl"}
```

A response log entry looks like:

```
DEBUG AsyncHttpClient -
<<< 200
content-type: application/json; charset=utf-8
content-length: 418
body: {"args":{},"data":{"from":"curl","message":"hello"},...,"url":"https://postman-echo.com/post"}
```

Logging is controlled by the `AsyncHttpClient` logger level. Add the following to your `log4j2.xml`:

```xml
<logger name="org.platformlambda.automation.http.AsyncHttpClient"
level="${env:HTTP_TRACE_LEVEL:-INFO}" additivity="false">
<AppenderRef ref="Console" />
</logger>
```

Set the `HTTP_TRACE_LEVEL` environment variable to `DEBUG` to enable tracing, or leave it unset
(defaults to `INFO`) to silence it. No code change or restart of the logging framework is needed —
only the environment variable controls the output.
Comment on lines +460 to +462
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation claims changing HTTP_TRACE_LEVEL requires no restart and that only the environment variable controls output. In most deployments (e.g., Docker/Kubernetes/systemd), environment variables are fixed at process start, so changing HTTP_TRACE_LEVEL typically requires restarting the application (or at least reloading the log4j2 configuration). Please reword to avoid implying runtime toggling via env var without restart; alternatively, describe using log4j2 configuration reload/monitorInterval if that’s the intended mechanism.

Suggested change
Set the `HTTP_TRACE_LEVEL` environment variable to `DEBUG` to enable tracing, or leave it unset
(defaults to `INFO`) to silence it. No code change or restart of the logging framework is needed —
only the environment variable controls the output.
Set the `HTTP_TRACE_LEVEL` environment variable to `DEBUG` before starting the application to enable
tracing, or leave it unset (defaults to `INFO`) to silence it. In most deployments, changes to
environment variables take effect only after restarting the application. If you need to change the
logger level without a restart, use Log4j2 configuration reload support for your logging
configuration.

Copilot uses AI. Check for mistakes.

## Starting a flow programmatically

To start an "event" flow from a unit test, you may use the helper class "FlowExecutor" under the "Event Script" module.
Expand Down
1 change: 1 addition & 0 deletions guides/CONFIGURATION-REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ virtual threads.
|-----|------|---------|-------------|
| `async.http.temp` | `String` (path) | `/tmp/async-http-temp` | Temporary folder used to buffer large async HTTP response bodies. |
| `http.client.connection.timeout` | `int` (ms) | `5000` | Connection timeout in milliseconds for the built-in async HTTP client. |
| `HTTP_TRACE_LEVEL` | `String` (env var) | `INFO` | Set to `DEBUG` to enable full HTTP request and response trace logging (method, URL, headers, body) via the `AsyncHttpClient` logger. Set to `INFO` or leave unset to silence. |

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ private void processRequest(Map<String, String> headers, EventEnvelope input, in
validateUrl(request);
String uri = request.getFinalizedUrl();
po.annotateTrace(DESTINATION, request.getTargetHost() + getRawUrl(uri));
if (log.isDebugEnabled()) {
logHttpRequest(request, uri);
}
HttpClient client = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
.headers(h -> updateHttpHeaders(po, request, h));
Comment on lines 176 to 182
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request log is emitted before updateHttpHeaders() runs, so it may not reflect the actual outbound request: (1) headers added later (sessionInfo, propagated trace header, and the computed Cookie header) are not shown, and (2) headers that will be filtered out by permittedHttpHeader() may still be logged. To make the trace accurate, build/log the final header set using the same filtering/augmentation logic used when sending the request (or log from within updateHttpHeaders after it has constructed the actual HttpHeaders).

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -453,6 +456,35 @@ private void removeExpiredFiles() {
}
}

private void logHttpRequest(AsyncHttpRequest request, String uri) {
var sb = new StringBuilder();
sb.append("\n>>> ").append(request.getMethod()).append(' ')
.append(request.getTargetHost()).append(uri).append('\n');
request.getHeaders().forEach((k, v) ->
sb.append(" ").append(k).append(": ").append(v).append('\n'));
Comment on lines +461 to +464
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP trace logging currently prints all request headers and their values verbatim. This can leak secrets (e.g., Authorization/Bearer tokens, cookies, API keys) into application logs when DEBUG is enabled. Please redact or omit sensitive headers (case-insensitive match on common names like authorization, cookie, set-cookie, x-api-key, proxy-authorization) and consider stripping any user-info from the logged URL as well.

Copilot uses AI. Check for mistakes.
Object body = request.getBody();
if (body instanceof byte[] b) {
sb.append(" body: [").append(b.length).append(" bytes]\n");
} else if (body instanceof Map || body instanceof List) {
sb.append(" body: ")
.append(SimpleMapper.getInstance().getMapper().writeValueAsString(body)).append('\n');
} else if (body instanceof String text && !text.isEmpty()) {
sb.append(" body: ").append(text).append('\n');
}
log.debug(sb.toString());
}

private void logHttpResponse(EventEnvelope response, byte[] b) {
var sb = new StringBuilder();
sb.append("\n<<< ").append(response.getStatus()).append('\n');
response.getHeaders().forEach((k, v) ->
sb.append(" ").append(k).append(": ").append(v).append('\n'));
if (b != null && b.length > 0) {
sb.append(" body: ").append(Utility.getInstance().getUTF(b)).append('\n');
}
Comment on lines +479 to +484
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logHttpResponse() always decodes the response body as UTF-8 text. sendFixedLengthResponse() can be used for non-text/binary responses whenever Content-Length is present, so this can produce unreadable output and potentially very large log messages. Consider logging body text only for known text content types (json/xml/text/javascript), otherwise log a byte count (and optionally truncate body logging to a maximum size) to avoid log bloat and accidental binary dumping.

Copilot uses AI. Check for mistakes.
log.debug(sb.toString());
}

private class HttpResponseHandler {
private final Utility util = Utility.getInstance();
private final CustomContentTypeResolver resolver = CustomContentTypeResolver.getInstance();
Expand Down Expand Up @@ -535,6 +567,9 @@ private void sendStreamResponse(EventEnvelope response, InputStream stream) {
}

private void sendFixedLengthResponse(String resContentType, EventEnvelope response, byte[] b) {
if (log.isDebugEnabled()) {
logHttpResponse(response, b);
}
if (resContentType != null) {
if (resContentType.startsWith(APPLICATION_JSON)) {
sendJsonResponse(response, b);
Expand Down
Loading