Add pipeline and fan-out-merge composite route strategies#192
Add pipeline and fan-out-merge composite route strategies#192
Conversation
…ests Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
…ategies Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
📋 API Contract Changes Summary✅ No breaking changes detected - only additions and non-breaking modifications Changed Components:Core FrameworkContract diff saved to artifacts/diffs/core.json Module: authContract diff saved to artifacts/diffs/auth.json Module: cacheContract diff saved to artifacts/diffs/cache.json Module: chimuxContract diff saved to artifacts/diffs/chimux.json Module: databaseContract diff saved to artifacts/diffs/database.json Module: eventbusContract diff saved to artifacts/diffs/eventbus.json Module: eventloggerContract diff saved to artifacts/diffs/eventlogger.json Module: httpclientContract diff saved to artifacts/diffs/httpclient.json Module: httpserverContract diff saved to artifacts/diffs/httpserver.json Module: jsonschemaContract diff saved to artifacts/diffs/jsonschema.json Module: letsencryptContract diff saved to artifacts/diffs/letsencrypt.json Module: logmaskerContract diff saved to artifacts/diffs/logmasker.json Module: reverseproxyContract diff saved to artifacts/diffs/reverseproxy.json Module: schedulerContract diff saved to artifacts/diffs/scheduler.json Artifacts📁 Full contract diffs and JSON artifacts are available in the workflow artifacts. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #192 +/- ##
==========================================
+ Coverage 56.66% 58.73% +2.07%
==========================================
Files 82 82
Lines 17660 17680 +20
==========================================
+ Hits 10007 10385 +378
+ Misses 6551 6231 -320
+ Partials 1102 1064 -38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds two new composite route strategies to the modules/reverseproxy module to support dependent multi-backend workflows (sequential request chaining) and ID-correlated parallel response merging, plus configurable “empty backend response” handling.
Changes:
- Introduces
pipelineandfan-out-mergecomposite strategies with new config/types and execution logic. - Adds per-route empty-response policy support via config (
empty_policy) and programmatic setters. - Adds unit tests + BDD scenarios, and updates the reverse-proxy example and README to document/illustrate usage.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| modules/reverseproxy/module.go | Stores per-route pipeline configs, fan-out mergers, and empty-response policies; adds setter APIs. |
| modules/reverseproxy/composite.go | Wires new strategies into CompositeHandler + dispatch; plumbs config/module wiring into handler creation. |
| modules/reverseproxy/composite_pipeline.go | Implements pipeline + fan-out-merge execution logic and supporting types/helpers. |
| modules/reverseproxy/config.go | Adds CompositeRoute.EmptyPolicy config field (empty_policy). |
| modules/reverseproxy/composite_pipeline_test.go | New unit tests covering pipeline/fan-out-merge and empty-policy behavior. |
| modules/reverseproxy/features/composite_pipeline.feature | New BDD feature scenarios for pipeline/fan-out-merge and empty-policy behavior. |
| modules/reverseproxy/bdd_step_registry_test.go | Registers new godog steps for the added feature scenarios. |
| modules/reverseproxy/bdd_composite_pipeline_test.go | Implements BDD step definitions for the new feature scenarios. |
| modules/reverseproxy/README.md | Documents all composite strategies (including the two new ones) and empty-policy usage. |
| examples/reverse-proxy/main.go | Adds runnable example usage for pipeline and fan-out-merge strategies and mock backends. |
| examples/reverse-proxy/config.yaml | Adds example backends and composite route config entries for the new strategies. |
|
|
||
| // Set empty response policy from config if specified | ||
| if routeConfig.EmptyPolicy != "" { | ||
| handler.SetEmptyResponsePolicy(EmptyResponsePolicy(routeConfig.EmptyPolicy)) |
There was a problem hiding this comment.
empty_policy is applied by casting the raw config string to EmptyResponsePolicy without validation. If a user misspells the value, the handler will silently fall back to the default behavior, making misconfigurations hard to detect. Consider validating routeConfig.EmptyPolicy against the allowed set (allow-empty/skip-empty/fail-on-empty) and returning a config error (or at least logging a warning and defaulting explicitly).
| handler.SetEmptyResponsePolicy(EmptyResponsePolicy(routeConfig.EmptyPolicy)) | |
| switch routeConfig.EmptyPolicy { | |
| case "allow-empty", "skip-empty", "fail-on-empty": | |
| handler.SetEmptyResponsePolicy(EmptyResponsePolicy(routeConfig.EmptyPolicy)) | |
| default: | |
| return nil, fmt.Errorf("invalid empty response policy %q for route %q: must be one of allow-empty, skip-empty, fail-on-empty", routeConfig.EmptyPolicy, routeConfig.Pattern) | |
| } |
| wg.Wait() | ||
|
|
||
| // Apply empty response policy | ||
| filteredResponses := make(map[string][]byte) | ||
| for backendID, body := range responses { | ||
| if isEmptyBody(body) { | ||
| switch h.emptyResponsePolicy { | ||
| case EmptyResponseFail: | ||
| w.WriteHeader(http.StatusBadGateway) | ||
| fmt.Fprintf(w, "Backend %s returned empty response", backendID) | ||
| return | ||
| case EmptyResponseSkip: |
There was a problem hiding this comment.
executeFanOutMerge will call the user-provided FanOutMerger even when no backend responses were collected (e.g., all requests failed or were skipped due to open circuit breakers). This differs from the existing merge/pipeline behavior which returns 502 when there are no successful responses, and it can lead to confusing 204/500 responses depending on the merger implementation. Consider short-circuiting with http.StatusBadGateway when len(responses)==0 (or after empty-policy filtering when len(filteredResponses)==0).
| return &http.Response{ | ||
| Status: http.StatusText(statusCode), | ||
| StatusCode: statusCode, | ||
| Header: http.Header{"Content-Type": []string{"application/json"}}, | ||
| Body: io.NopCloser(bytes.NewReader(body)), | ||
| }, nil |
There was a problem hiding this comment.
MakeJSONResponse sets http.Response.Status to http.StatusText(statusCode) (e.g., "OK"), but per net/http conventions it should be the full status line (e.g., "200 OK") or left empty so it can be derived from StatusCode. Returning a response with an invalid Status can break callers that write the response via (*http.Response).Write. Consider setting Status to fmt.Sprintf("%d %s", statusCode, http.StatusText(statusCode)) or leaving it empty.
Composite routes lacked support for inter-backend data dependencies — e.g., fetching conversation IDs from backend A, then querying backend B with those IDs for follow-up details. Also no way to correlate parallel responses by ID across backends.
New Strategies
pipeline— Sequential execution where each stage's response constructs the next stage's request viaPipelineRequestBuilder. OptionalPipelineResponseMergerassembles the final response.fan-out-merge— Parallel execution (likemerge) but with a customFanOutMergerfunction for ID-based matching, filtering, and correlation across responses.Empty Response Policies
Configurable per-route via config (
empty_policy) orSetEmptyResponsePolicy():allow-emptyskip-emptyfail-on-emptyUsage
Changes
composite_pipeline.go— New types (PipelineConfig,PipelineRequestBuilder,PipelineResponseMerger,FanOutMerger,EmptyResponsePolicy), execution logic,MakeJSONResponsehelpercomposite.go— ExtendedCompositeHandlerwith pipeline/fan-out-merge fields,ServeHTTPdispatch, setter methods, config wiring increateCompositeHandlermodule.go— Storage maps andSetPipelineConfig/SetFanOutMerger/SetEmptyResponsePolicyon the moduleconfig.go—EmptyPolicyfield onCompositeRoutecomposite_pipeline_test.go) + 5 BDD scenarios (features/composite_pipeline.feature)examples/reverse-proxy/with pipeline and fan-out-merge backends, config, and registrationsWarning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
tenant-secondary/tmp/go-build1261898992/b001/reverseproxy.test /tmp/go-build1261898992/b001/reverseproxy.test -test.paniconexit0 -test.v=true -test.count=1 -test.timeout=5m0s 7362309/b001/reverseproxy.test --log-level e/git --log-target journal-or-kmsg(dns block)If you need me to access, download, or install something from one of these locations, you can either:
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.