Skip to content

Commit 5fbc50f

Browse files
committed
feat(http): add bodytext field to response object to always return response body as string
Signed-off-by: Yiran Cao <yiran0427@gmail.com>
1 parent 9cd0158 commit 5fbc50f

File tree

3 files changed

+100
-22
lines changed

3 files changed

+100
-22
lines changed

docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/http.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ is structured as follows:
109109
| `headers` | `http.Header` | The headers of the response. See applicable [Go documentation](https://pkg.go.dev/net/http#Header). |
110110
| `header` | `func(string) string` | `headers` can be inconvenient to work with directly. This function allows you to access a header by name. |
111111
| `body` | `map[string]any` | The body of the response, if any, unmarshaled into a map. If the response body is empty, this map will also be empty. |
112+
| `bodyText` | `string` | The raw body of the response as a string. This is useful for non-JSON responses like plain text, tokens, or other string formats. |
112113

113114
## Outputs
114115

pkg/promotion/runner/builtin/http_requester.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -289,25 +289,33 @@ func (h *httpRequester) buildExprEnv(
289289
// TODO(krancour): Casting as an int64 is a short-term fix here because
290290
// deep copy of the output map will panic if any value is an int. This is
291291
// a near-term fix and a better solution will be PR'ed soon.
292-
"status": int64(resp.StatusCode),
293-
"header": resp.Header.Get,
294-
"headers": resp.Header,
295-
"body": map[string]any{},
292+
"status": int64(resp.StatusCode),
293+
"header": resp.Header.Get,
294+
"headers": resp.Header,
295+
"body": map[string]any{},
296+
"bodyText": string(bodyBytes),
296297
},
297298
}
298299
contentType, _, _ := mime.ParseMediaType(resp.Header.Get(contentTypeHeader))
299300
if len(bodyBytes) > 0 && (contentType == contentTypeJSON || json.Valid(bodyBytes)) {
300301
var parsedBody any
301302
if err := json.Unmarshal(bodyBytes, &parsedBody); err != nil {
302-
return nil, fmt.Errorf("failed to parse response: %w", err)
303-
}
304-
305-
// Unmarshal into map[string]any or []any
306-
switch parsedBody.(type) {
307-
case map[string]any, []any:
308-
env["response"].(map[string]any)["body"] = parsedBody // nolint: forcetypeassert
309-
default:
310-
return nil, fmt.Errorf("unexpected type when unmarshaling response: %T", parsedBody)
303+
// Log the JSON parsing error but continue to provide bodyText
304+
logging.LoggerFromContext(ctx).Debug(
305+
"Failed to parse JSON response, continuing with bodyText only",
306+
"error", err,
307+
)
308+
} else {
309+
// Unmarshal into map[string]any or []any
310+
switch parsedBody.(type) {
311+
case map[string]any, []any:
312+
env["response"].(map[string]any)["body"] = parsedBody // nolint: forcetypeassert
313+
default:
314+
logging.LoggerFromContext(ctx).Debug(
315+
"Unexpected JSON type, continuing with bodyText only",
316+
"type", fmt.Sprintf("%T", parsedBody),
317+
)
318+
}
311319
}
312320
}
313321

pkg/promotion/runner/builtin/http_requester_test.go

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ func Test_httpRequester_run(t *testing.T) {
230230
Name: "theMeaningOfLife",
231231
FromExpression: "response.body.theMeaningOfLife",
232232
},
233+
{
234+
Name: "bodyText",
235+
FromExpression: "response.bodyText",
236+
},
233237
},
234238
},
235239
assertions: func(t *testing.T, res promotion.StepResult, err error) {
@@ -240,6 +244,7 @@ func Test_httpRequester_run(t *testing.T) {
240244
map[string]any{
241245
"status": int64(http.StatusOK),
242246
"theMeaningOfLife": nil,
247+
"bodyText": "",
243248
},
244249
res.Output,
245250
)
@@ -263,6 +268,10 @@ func Test_httpRequester_run(t *testing.T) {
263268
Name: "theMeaningOfLife",
264269
FromExpression: "response.body.theMeaningOfLife",
265270
},
271+
{
272+
Name: "bodyText",
273+
FromExpression: "response.bodyText",
274+
},
266275
},
267276
},
268277
assertions: func(t *testing.T, res promotion.StepResult, err error) {
@@ -273,6 +282,7 @@ func Test_httpRequester_run(t *testing.T) {
273282
map[string]any{
274283
"status": int64(http.StatusOK),
275284
"theMeaningOfLife": nil,
285+
"bodyText": "this is just a regular string",
276286
},
277287
res.Output,
278288
)
@@ -296,6 +306,10 @@ func Test_httpRequester_run(t *testing.T) {
296306
Name: "theMeaningOfLife",
297307
FromExpression: "response.body.theMeaningOfLife",
298308
},
309+
{
310+
Name: "bodyText",
311+
FromExpression: "response.bodyText",
312+
},
299313
},
300314
},
301315
assertions: func(t *testing.T, res promotion.StepResult, err error) {
@@ -306,6 +320,7 @@ func Test_httpRequester_run(t *testing.T) {
306320
map[string]any{
307321
"status": int64(http.StatusOK),
308322
"theMeaningOfLife": float64(42),
323+
"bodyText": `{"theMeaningOfLife": 42}`,
309324
},
310325
res.Output,
311326
)
@@ -329,6 +344,10 @@ func Test_httpRequester_run(t *testing.T) {
329344
Name: "theMeaningOfLife",
330345
FromExpression: "response.body[0].theMeaningOfLife",
331346
},
347+
{
348+
Name: "bodyText",
349+
FromExpression: "response.bodyText",
350+
},
332351
},
333352
},
334353
assertions: func(t *testing.T, res promotion.StepResult, err error) {
@@ -339,6 +358,7 @@ func Test_httpRequester_run(t *testing.T) {
339358
map[string]any{
340359
"status": int64(http.StatusOK),
341360
"theMeaningOfLife": float64(42),
361+
"bodyText": `[{"theMeaningOfLife": 42}]`,
342362
},
343363
res.Output,
344364
)
@@ -543,6 +563,11 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
543563
body, ok := bodyAny.(map[string]any)
544564
require.True(t, ok)
545565
require.Empty(t, body)
566+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
567+
require.True(t, ok)
568+
bodyText, ok := bodyTextAny.(string)
569+
require.True(t, ok)
570+
require.Equal(t, "", bodyText)
546571
},
547572
},
548573
{
@@ -554,11 +579,18 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
554579
},
555580
assertions: func(t *testing.T, env map[string]any, err error) {
556581
require.NoError(t, err)
582+
557583
bodyAny, ok := env["response"].(map[string]any)["body"]
558584
require.True(t, ok)
559585
body, ok := bodyAny.(map[string]any)
560586
require.True(t, ok)
561587
require.Equal(t, map[string]any{"foo": "bar"}, body)
588+
589+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
590+
require.True(t, ok)
591+
bodyText, ok := bodyTextAny.(string)
592+
require.True(t, ok)
593+
require.Equal(t, `{"foo": "bar"}`, bodyText)
562594
},
563595
},
564596
{
@@ -570,17 +602,24 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
570602
},
571603
assertions: func(t *testing.T, env map[string]any, err error) {
572604
require.NoError(t, err)
573-
bodyAny, ok := env["response"].(map[string]any)["body"]
574-
require.True(t, ok)
575605

576606
// Check if interface is of type []any
607+
bodyAny, ok := env["response"].(map[string]any)["body"]
608+
require.True(t, ok)
577609
body, ok := bodyAny.([]any)
578610
require.True(t, ok)
579611
require.Len(t, body, 2)
580612

581613
firstItem, ok := body[0].(map[string]any)
582614
require.True(t, ok)
583615
require.Equal(t, map[string]any{"foo1": "bar1"}, firstItem)
616+
617+
// Check bodyText contains the raw JSON array string
618+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
619+
require.True(t, ok)
620+
bodyText, ok := bodyTextAny.(string)
621+
require.True(t, ok)
622+
require.Equal(t, `[{"foo1": "bar1"}, {"foo2": "bar2"}]`, bodyText)
584623
},
585624
},
586625
{
@@ -590,9 +629,21 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
590629
Header: http.Header{"Content-Type": []string{"application/json"}},
591630
Body: io.NopCloser(strings.NewReader(`{"foo":`)),
592631
},
593-
assertions: func(t *testing.T, _ map[string]any, err error) {
594-
require.Error(t, err)
595-
require.ErrorContains(t, err, "failed to parse response")
632+
assertions: func(t *testing.T, env map[string]any, err error) {
633+
require.NoError(t, err)
634+
require.NotNil(t, env)
635+
636+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
637+
require.True(t, ok)
638+
bodyText, ok := bodyTextAny.(string)
639+
require.True(t, ok)
640+
require.Equal(t, `{"foo":`, bodyText)
641+
642+
bodyAny, ok := env["response"].(map[string]any)["body"]
643+
require.True(t, ok)
644+
body, ok := bodyAny.(map[string]any)
645+
require.True(t, ok)
646+
require.Empty(t, body)
596647
},
597648
},
598649
{
@@ -602,9 +653,21 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
602653
Header: http.Header{"Content-Type": []string{"application/json"}},
603654
Body: io.NopCloser(strings.NewReader(`"foo"`)),
604655
},
605-
assertions: func(t *testing.T, _ map[string]any, err error) {
606-
require.Error(t, err)
607-
require.ErrorContains(t, err, "unexpected type when unmarshaling")
656+
assertions: func(t *testing.T, env map[string]any, err error) {
657+
require.NoError(t, err)
658+
require.NotNil(t, env)
659+
660+
bodyAny, ok := env["response"].(map[string]any)["body"]
661+
require.True(t, ok)
662+
body, ok := bodyAny.(map[string]any)
663+
require.True(t, ok)
664+
require.Empty(t, body)
665+
666+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
667+
require.True(t, ok)
668+
bodyText, ok := bodyTextAny.(string)
669+
require.True(t, ok)
670+
require.Equal(t, `"foo"`, bodyText)
608671
},
609672
},
610673
{
@@ -616,12 +679,18 @@ func Test_httpRequester_buildExprEnv(t *testing.T) {
616679
},
617680
assertions: func(t *testing.T, env map[string]any, err error) {
618681
require.NoError(t, err)
682+
619683
bodyAny, ok := env["response"].(map[string]any)["body"]
620684
require.True(t, ok)
621-
622685
body, ok := bodyAny.(map[string]any)
623686
require.True(t, ok)
624687
require.Equal(t, map[string]any{"foo": "bar"}, body)
688+
689+
bodyTextAny, ok := env["response"].(map[string]any)["bodyText"]
690+
require.True(t, ok)
691+
bodyText, ok := bodyTextAny.(string)
692+
require.True(t, ok)
693+
require.Equal(t, `{"foo": "bar"}`, bodyText)
625694
},
626695
},
627696
}

0 commit comments

Comments
 (0)