Skip to content

Commit d6f48ab

Browse files
FEAT: Support for forwarding response headers from http task (#36)
* FEAT: Support for forwarding response headers from http task
1 parent 93a724b commit d6f48ab

5 files changed

Lines changed: 42 additions & 2 deletions

File tree

internal/pkg/config/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
const (
1616
templateName = `render`
17-
placeholderRegex = "([a-zA-Z0-9_]+?)"
17+
placeholderRegex = "([a-zA-Z0-9_-]+?)"
1818
)
1919

2020
// Generic String type that can evaluate both macro and context templates

internal/pkg/jq/jq.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ func (q *Query) Execute(document []byte, inputs ...any) (any, error) {
5050
var data any
5151

5252
if err := json.Unmarshal(document, &data); err != nil {
53-
return nil, err
53+
// If the document is not valid JSON, treat it as a raw string
54+
data = string(document)
5455
}
5556

5657
values := make([]any, 0, len(inputs)+1)

internal/pkg/pipeline/task/http/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ The HTTP task operates in two modes depending on whether an input channel is pro
1616

1717
In both modes, the task sends HTTP response data to its output channel and supports automatic retries, OAuth authentication, and proxy configuration.
1818

19+
### Response Format and Headers
20+
21+
The HTTP task outputs the response body as-is (maintains backward compatibility). Response headers are automatically stored in the record's context with the prefix `http-header-`, making them accessible to downstream tasks via context variables.
22+
23+
For example, if the HTTP response includes a `Content-Type` header, it will be available as `{{ context "http-header-Content-Type" }}` in subsequent tasks. Note that HTTP header names are case-sensitive when used as context keys. Go's HTTP library canonicalizes header names (for example, `content-type` becomes `Content-Type`), so you must use the canonical form when accessing headers via context (for example, `http-header-Content-Type`, not `http-header-content-type`).
24+
1925
## Configuration Fields
2026

2127
| Field | Type | Default | Description |
@@ -33,6 +39,7 @@ In both modes, the task sends HTTP response data to its output channel and suppo
3339
| `oauth` | object | - | OAuth configuration (see OAuth section) |
3440
| `proxy` | object | - | Proxy configuration |
3541
| `next_page` | string/object | - | JQ expression to extract next page URL (string) or pagination config (object with`endpoint`, `method`, `body`, `headers`) |
42+
| `context` | map[string]string | - | JQ expressions to extract values from the response and store in record context |
3643
| `fail_on_error` | bool | `false` | Whether to stop the pipeline if this task encounters an error |
3744

3845
## OAuth Configuration
@@ -96,6 +103,27 @@ tasks:
96103
Authorization: Bearer {{ context "auth_token" }}
97104
```
98105
106+
### Setting context values from response and accessing headers:
107+
```yaml
108+
tasks:
109+
- name: fetch_user
110+
type: http
111+
method: GET
112+
endpoint: https://api.example.com/user/123
113+
context:
114+
user_name: ".data | fromjson | .name"
115+
user_email: ".data | fromjson | .email"
116+
117+
- name: use_context
118+
type: jq
119+
path: |
120+
{
121+
"greeting": "Hello {{ context "user_name" }}",
122+
"email": "{{ context "user_email" }}",
123+
"content_type": "{{ context "http-header-Content-Type" }}"
124+
}
125+
```
126+
99127
## Sample Pipelines
100128
101129
- `test/pipelines/convert_industries.yaml` - HTTP GET request to fetch CSV data

internal/pkg/pipeline/task/http/http.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"strings"
1011
"time"
1112

1213
"github.com/patterninc/caterpillar/internal/pkg/config"
@@ -22,6 +23,7 @@ const (
2223
defaultExpectedStatuses = `200`
2324
defaultMethod = http.MethodGet
2425
defaultTimeout = duration.Duration(90 * time.Second)
26+
headerContextPrefix = "http-header-%s"
2527
)
2628

2729
var (
@@ -170,6 +172,13 @@ func (h *httpCore) processItem(rc *record.Record, output chan<- *record.Record)
170172
}
171173

172174
if output != nil {
175+
// Store headers in the context for downstream tasks to access
176+
// Header names are preserved as-is and stored with the http-header- prefix
177+
for headerName, headerValues := range result.Headers {
178+
contextKey := fmt.Sprintf(headerContextPrefix, headerName)
179+
rc.SetContextValue(contextKey, strings.Join(headerValues, "; "))
180+
}
181+
173182
h.SendData(rc.Context, []byte(result.Data), output)
174183
}
175184

test/pipelines/context_test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ tasks:
3030
"user_name": "{{ context "user_name" }}",
3131
"user_email": "{{ context "user_email" }}",
3232
"user_company": "{{ context "user_company" }}",
33+
"headers_content_type": "{{ context "http-header-Content-Type" }}",
34+
"headers_age": "{{ context "http-header-Age" }}",
3335
"original_data": .
3436
}
3537

0 commit comments

Comments
 (0)