Skip to content

Fix dispatcher GrailsWebRequest publication after late filters - #16873

Open
ruthst00 wants to merge 1 commit into
apache:7.0.xfrom
ruthst00:fix/15819-dispatcher-grailswebrequest
Open

ruthst00 wants to merge 1 commit into
apache:7.0.xfrom
ruthst00:fix/15819-dispatcher-grailswebrequest

Conversation

@ruthst00

@ruthst00 ruthst00 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

The Fix

Fixes #15819

Splits the dispatcher fix from #16516 as requested by @matrei:

  • Publish the dispatcher's GrailsWebRequest when a late filter replaces the binding.
  • Preserve the request and response wrappers seen by the dispatcher.
  • Keep Callable request bindings independent of the initial dispatch.
  • Add dispatcher and late-filter regression coverage.
  • Retain only the results rows in the example application.

NOTE: The adapter guard and associated API/documentation changes in #16516 are reserved for a separate PR against 9.0.x which will be created after this PR is merged.

Generated with GPT 6 Astra via Cline API Provider in IntelliJ IDEA 2025.3.3

Contributor Checklist

Please review the following checklist before submitting your pull request. Pull requests that do not meet these requirements may be closed without review.

Issue and Scope

  • This PR is linked to an existing issue that has been acknowledged or approved by the project team. If no approved issue exists, please give background on why this change is necessary. Tickets are preferred for release change log history.
  • This PR addresses the complete scope of the linked issue. Partial implementations or unfinished work should not be submitted for review.
  • This PR contains a single, focused change. Unrelated changes should be submitted as separate pull requests.
  • This PR targets the correct branch for the type of change:
    • Patch release branches (e.g., 7.0.x): Bug fixes only. No new features or API changes.
    • Minor release branches (e.g., 7.1.x): New features are welcome, but breaking existing APIs must be avoided.
    • Major release branches (e.g., 8.0.x): Reserved for major changes. Breaking API changes are permitted.

Code Quality

  • I have added or updated tests that cover the changes introduced in this PR. All code contributions are expected to include appropriate test coverage.
  • I have verified that all existing tests pass by running ./gradlew build --rerun-tasks.
  • My code follows the project's code style guidelines. I have run ./gradlew codeStyle and resolved any violations. See Code Style for details.
  • This PR does not include mass reformatting, style-only changes, or large-scale refactoring unless it was explicitly approved in the linked issue. Unsolicited reformatting will not be accepted.
  • If generative AI tooling was used in preparing this contribution, a quality model was used to ensure contributions are consistent with the project's quality standards.

Licensing and Attribution

Documentation

  • If this PR introduces user-facing changes, I have included or updated the relevant documentation.
  • If this PR adds a new feature, I have updated the What's New section of the Grails Guide.
  • If this PR introduces breaking changes or changes that require user action during an upgrade, I have updated the Upgrade Notes for the corresponding version in the Grails Guide.
  • The PR description clearly explains what was changed and why.

@ruthst00

ruthst00 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

@matrei, @jdaugherty, @jamesfredley, as proposed by @matrei as a comment on the original PR, this is the first of two new PRs which split the original PR into the simpler GrailsDispatcherServlet bug, targeted for 8.0.x, and the adapter guard related changes, targeted for 9.0.x

The Second PR will be submitted after the first PR is merged with the 9.0.x branch since the adapter’s late-filter tests depend on fixes in this PR.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approved. A later filter that wraps the request is ignored while the Grails binding is still active, so the controller sees the earlier request.

protected ServletRequestAttributes buildRequestAttributes(HttpServletRequest request, HttpServletResponse response, RequestAttributes previousAttributes) {
if (previousAttributes == null || !(previousAttributes instanceof GrailsWebRequest)) {
return buildGrailsWebRequest(request, response)
if (previousAttributes instanceof GrailsWebRequest && previousAttributes.active) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Grails binding is request R. A later filter passes a wrapper of R whose isUserInRole('ADMIN') returns true, and leaves the binding in place. This return reuses the previous attributes. lookup() then returns R, so the controller sees false. Publish the wrapper the dispatcher received.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 49840cf

jamesfredley
jamesfredley previously approved these changes Nov 6, 2026

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A later filter that wraps the request now causes a new Grails web request to be published with that wrapper. The controller sees the wrapper, including isUserInRole.

@testlens-app

testlens-app Bot commented Nov 6, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 49840cf
▶️ Tests: 109889 executed
⚪️ Checks: 99/99 completed


Learn more about TestLens at testlens.app/docs.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approving. A filter that adds a parameter loses that value when a later filter wraps the request.

ruthst00 added a commit to ruthst00/grails-core that referenced this pull request Nov 9, 2026
- Preserves filter-added `params` when later filters wrap the request/response or replace the thread binding.
- Retains those values through URL mapping into controller rendering.
- Keeps completed requests and async workers isolated.
- Expanded regression tests and updated the `params` documentation.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A filter that adds a parameter is kept when a later filter wraps the request. Thread PRRT_kwDOAAfRJ861bcuw resolved. Not approving. A wrapper that replaces a parameter is overwritten with the original value.

if (previousWebRequest != null && previousWebRequest.active) {
// URL mappings reset params before configuring a match, so retain filter changes in
// the baseline as well as the dispatch's mutable parameter map.
webRequest.originalParams.putAll(previousWebRequest.params)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

GET /test?name=raw, then a wrapper whose parameter getters return name=wrapped. The controller sees params.name == raw. The wrapper's value is lost. Keep the wrapper's value unless a filter changed that parameter itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 359c6b3

ruthst00 added a commit to ruthst00/grails-core that referenced this pull request Nov 12, 2026
- Preserves filter-added `params` when later filters wrap the request/response or replace the thread binding.
- Retains those values through URL mapping into controller rendering.
- Keeps completed requests and async workers isolated.
- Expanded regression tests and updated the `params` documentation.
@ruthst00
ruthst00 force-pushed the fix/15819-dispatcher-grailswebrequest branch from 359c6b3 to fe68244 Compare November 12, 2026 17:31
ruthst00 added a commit to ruthst00/grails-core that referenced this pull request Nov 12, 2026
- Preserves filter-added `params` when later filters wrap the request/response or replace the thread binding.
- Retains those values through URL mapping into controller rendering.
- Keeps completed requests and async workers isolated.
- Expanded regression tests and updated the `params` documentation.
@ruthst00
ruthst00 force-pushed the fix/15819-dispatcher-grailswebrequest branch from fe68244 to 8385e3b Compare November 12, 2026 18:36

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approving. A filter that changes one value of a multi-value parameter loses that change when a later wrapper replaces the parameter.

if (value instanceof Map && originalValue instanceof Map && targetValue instanceof Map) {
mergeParameterChanges((Map) targetValue, (Map) originalValue, (Map) value)
}
else if (!original.containsKey(key) || !Objects.deepEquals(originalValue, value)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A filter sets params.tags[0] to filter on a request whose tags are raw and unchanged. A later wrapper supplies wrapped and values. The controller receives the wrapper's values. Keep the filter's edit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in bd43f4f

@jamesfredley
jamesfredley dismissed their stale review November 13, 2026 12:06

The approval no longer holds: a filter's edit of one value is kept, but the wrapper's other value in that parameter is dropped.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approving. A filter's edit of one value is kept, but the wrapper's other value in that parameter is dropped. Cloning each array also splits the flat name from the nested name.

mergeParameterChanges((Map) targetValue, (Map) originalValue, (Map) value)
}
else if (!original.containsKey(key) || !Objects.deepEquals(originalValue, value)) {
target.put(key, value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request tags are raw and unchanged. A filter sets params.tags[0] to filter. A later wrapper supplies wrapped and values. The controller should receive filter and values. It receives filter and unchanged, because any difference replaces the whole array. Apply the change at the index that changed, and leave the wrapper's other value. The same comparison also treats an unchanged cloned array as a filter edit, so a wrapper that removes book.tags still leaves the raw values on params.book.tags.

if (entry.getValue() instanceof GrailsParameterMap) {
entry.setValue(((GrailsParameterMap) entry.getValue()).clone());
}
else if (entry.getValue() instanceof String[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

book.tags starts as one array shared by the flat name and the nested name. Cloning each occurrence separately splits them. Setting params['book.tags'][0] then leaves params.book.tags[0] unchanged, with no later filter involved. Keep those two names on the same array inside the clone, and keep that array off the original.

@matrei matrei left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of #16873 at bd43f4fe630 (base 8.0.x). It is 5 commits on 47d8021e481, 3 commits behind the 8.0.x tip 88272db2f39, and it merges into the tip cleanly.

What I ran, on the PR merged into the 8.0.x tip unless it says 7.0.x:

  • Unit suites, rerun without the build cache: grails-web-common (343), grails-web-mvc (99), grails-web-url-mappings (380) and grails-controllers (214). No failures.
  • On 7.0.x (356cde03177): the example app with and without the first commit's dispatcher change and with and without the late filter, a probe action and filter added to it, and grails-web-mvc (20), grails-web-url-mappings (163), grails-controllers (22) and grails-test-suite-web (379) with the change. No failures. The results are in 1.
  • :grails-test-examples-issue-15819:integrationTest in four setups. The results are in 1.
  • The PR's new specs against 8.0.x: GrailsDispatcherServletSpec, GrailsDispatcherServletHiddenMethodSpec, UrlMappingsHandlerMappingSpec and GrailsParameterMapTests, with GrailsDispatcherServlet and GrailsParameterMap back at 8.0.x.
  • A probe spec, not committed. GrailsWebRequestFilter, then a filter that changes params, then optionally a later filter, then GrailsDispatcherServlet with UrlMappingsHandlerMapping and a Grails action, plus a URL mapping that redirects with keepParamsWhenRedirect. I ran it at the merge, with both main files back at 8.0.x, and with the dispatcher from the first commit. The results are in 2 and 3.

#15819 no longer happens on 8.0.x. #16797, merged after my #16516 review, already fixed it there. So on 8.0.x this PR doesn't fix the bug. What it does do there is change behavior for apps that have no late RequestContextFilter, and make params depend on whether some unrelated filter wraps the request. The bug is still on 7.0.x, where the example app fails, and 7.1.x and 7.2.x have the same code. I'd retarget the PR to 7.0.x and keep only the first commit, 4bc3f4bdc6a, there, with one change so it compiles.

1. #15819 is already fixed on 8.0.x

The bug was a split between two GrailsWebRequest instances: ResponseRenderer cleared renderView on the instance bound to the thread, while the adapter read the one in the request attribute through GrailsWebRequest.lookup(request). #16797 (c4714bb1e0b, b35184a8f84) changed lookup(request) to return the instance bound to the thread whenever it wraps the same container request. Since then the adapter reads the dispatcher's instance, the same one ResponseRenderer uses.

8.0.x tip, with example app, both results rows
GrailsDispatcherServlet and GrailsParameterMap as on 8.0.x pass
the same, but lookup(request) as before #16797 fail
the same, plus the first commit's GrailsDispatcherServlet pass
this PR pass

The PR's specs show the same thing. With both main files at 8.0.x:

  • GrailsDispatcherServletSpec: 7 of 25 fail, all of them rows of a late filter preserves dispatcher wrappers .... The 3 replaceBinding=false rows fail on the wrapper (see 4), and the 4 replaceBinding=true rows fail on params.retained (see 2). Every row of service selects an active request ..., nested #dispatchType through a late filter ... and the Callable feature passes.
  • UrlMappingsHandlerMappingSpec: the template is rendered in all 4 renderTemplate rows. Two fail on the filter's name in the model (see 2), and two on renderingRequest.is(attributeRequest), so on 8.0.x the only difference left is which instance is in the request attribute.
  • GrailsParameterMapTests: testCloneIsolatesMultiValuedParameters fails (see 3).

So none of the PR's tests for #15819 can fail on 8.0.x. They're fine as regression tests, but the dispatcher change isn't needed there.

7.0.x, 7.1.x and 7.2.x still have the old lookup(request) and the same buildRequestAttributes. I ran the example app on 7.0.x with a probe action next to it that reports what the controller gets and handles an upload:

7.0.x example app upload (request.getFile, params.doc) lookup() is the request attribute
no late filter pass works yes
late filter fail, 500 both missing no
late filter, first commit's change pass works yes

So behind a late filter, uploads are broken as well, for the same reason: checkMultipart hands the multipart request to the instance in the request attribute, and the controller reads the other one. URL-mapped params.id, actionName, a conventional view, a redirect and the 500 page work in all three rows. Without the late filter the probe gives the same results with and without the change. That's what the code says too: with no previous binding, or an active GrailsWebRequest, the first commit does exactly what 7.0.x does.

The first commit doesn't compile on 7.0.x as it is. There grails-web-common has grails-gsp-core only as compileOnly, and GrailsApplicationAttributes declares a grails-gsp-core type (getPagesTemplateEngine()). So GrailsApplicationAttributes.WEB_REQUEST in GrailsDispatcherServlet fails with NoClassDefFoundError: org.grails.gsp.ResourceAwareTemplateEngine during compilation. WebUtils.storeGrailsWebRequest(webRequest) compiles and does the same here. It also binds the instance to the thread, which FrameworkServlet does right after buildRequestAttributes returns anyway. The four 7.0.x suites above pass with that change.

My suggestion: retarget to 7.0.x with the first commit's buildRequestAttributes, publishing through WebUtils.storeGrailsWebRequest, plus the example app with an upload added, and tests for the dispatcher without the rows for 2-4. GrailsDispatcherServletSpec and GrailsDispatcherServletHiddenMethodSpec don't exist on 7.0.x, so those tests need a new spec there. The merge-up then carries the fix and the regression tests to 8.0.x. Everything else should go to 9.0.x, if we want it at all, for the reasons below. If you'd rather keep this on 8.0.x, it should come down to the regression tests.

The changes after the first commit came from @jamesfredley's review rounds, and each one asks for something reasonable. But 8.0.x doesn't do any of it for an app without a late filter either, so doing it only behind one makes those apps the odd ones out.

2. A filter's changes to params reach the action only when a later filter wraps the request or replaces the binding

On 8.0.x, a URL-mapped action never sees a change a servlet filter made to params. Before it matches, UrlMappingsHandlerMapping calls webRequest.resetParams() (UrlMappingsHandlerMapping.groovy:244), which rebuilds params from originalParams, the request's own parameters. The scenarios in the review rounds don't show this, because they map /test with a SimpleUrlHandlerMapping, and that doesn't reset anything.

This PR copies the filter's changes into the new instance's originalParams (GrailsDispatcherServlet.groovy:76-77), so they survive the reset. But it only does that when the dispatcher builds its own instance. In the probe, the request has name=raw and tags=raw&tags=unchanged. The filter sets params.added and params.name to 'filter', and sets params.tags[0] = 'filter'. The action reports added, name and tags:

after the filter 8.0.x this PR
nothing null, raw, [filter, unchanged] null, raw, [raw, unchanged]
a later RequestContextFilter null, raw, [filter, unchanged] filter, filter, [filter, unchanged]
a later filter that only wraps the request null, raw, [filter, unchanged] filter, filter, [filter, unchanged]
a later filter that only wraps the response null, raw, [filter, unchanged] filter, filter, [filter, unchanged]
any of these, on a multipart POST null, raw, [filter, unchanged] null, raw, [raw, unchanged]

On 8.0.x every row is the same, and so is every row with the first commit's dispatcher. With this PR, the same action gets different params depending on whether a later filter wraps the request or the response. Filters like that are common, see 4. Two more effects:

  • A URL mapping that redirects with keepParamsWhenRedirect appends originalParams to the redirect (ResponseRedirector.groovy:111-113). With a later filter, the filter's values end up in the Location header, including any it added for its own use: /probe/show?name=filter&tags=filter&tags=unchanged&added=filter, where 8.0.x sends ?name=raw&tags=filter&tags=unchanged.
  • On a multipart request, checkMultipart calls multipartRequestResolved() (GrailsDispatcherServlet.groovy:144), which discards both maps, so the copied changes are lost again (last row).

The new params.adoc paragraph documents this as a property of params. It holds neither in an app without a later filter nor on a multipart request. If the changes a filter makes to params should reach the action, that's a feature for every app, so it belongs in 9.0.x, with docs and an upgrade note. There it would also have to survive resetParams() without a late filter.

3. GrailsParameterMap.clone() now copies arrays, which changes params in every app

resetParams() clones originalParams. On 8.0.x the clone shares the String[] values, so an in-place edit such as params.tags[0] = 'filter' also changes originalParams, and survives every later resetParams(). That's the [filter, unchanged] in the 8.0.x column above, and it's why the new GrailsParameterMapTests test fails on 8.0.x. The PR copies the arrays (GrailsParameterMap.java:145-147), so in an app without a later filter, the filter's edit is now lost when the URL mappings reset params: the first row of the PR column.

I'd call the sharing accidental. But it's what every app gets today, and the copy is only needed as the baseline for the comparison in 2. Once 2 is gone, this can go too.

4. A filter that only wraps the request or response now gets its own GrailsWebRequest

This came in with the first review round. On 8.0.x, when a later filter wraps the request or the response but leaves the binding alone, the dispatcher reuses the filter's instance. Nothing breaks there, because RequestContextHolder and the request attribute hold the same instance, so it isn't #15819. The controller sees the request and response GrailsWebRequestFilter saw, like in every other app. With this PR it gets a new instance built with the wrappers, and every request in such an app goes through the copying from 2.

In #16516 I suggested that giving the late-filter apps the same request and response as every other app should wait for 9.0.x. This is the same kind of change, just the other way round: apps without a late RequestContextFilter now get a different request and response than they get on 8.0.x. Wrapping filters after GrailsWebRequestFilter (-150) aren't rare. A FilterRegistrationBean or @Component filter without an order runs at LOWEST_PRECEDENCE, for example a logging filter with a ContentCachingRequestWrapper. Two examples of what changes:

  • Spring Security's FilterChainProxy runs at -100 and wraps the request and response on every request. On the plugin's default chains nothing changes, because UpdateRequestContextHolderExceptionTranslationFilter already binds a DelegatingGrailsWebRequest built with those wrappers, and the dispatcher receives the same objects. On a chain without that filter, such as the REST plugin's documented /api/** chain (-exceptionTranslationFilter), request.userPrincipal, request.remoteUser and request.isUserInRole(...) in a controller now answer from Spring Security instead of the container.
  • A response wrapper such as ContentCachingResponseWrapper now gets the action's output. On 8.0.x the output goes to the response underneath it.

I got both from reading the code, not from running an app. The PR's own spec asserts the underlying behavior in the replaceBinding=false rows of a late filter preserves dispatcher wrappers ....

Both may well be what those apps want, but they're changes in behavior on a patch branch, and they're unrelated to #15819. The first commit reuses any active GrailsWebRequest, as 8.0.x does.

5. Tests

  • With 2-4 gone, the replaceBinding=false rows and the params assertions in a late filter preserves dispatcher wrappers ... go too, and so do the params assertions and the filterChangesName rows of the UrlMappingsHandlerMappingSpec feature.
  • GrailsDispatcherServletHiddenMethodSpec: the helper now asserts GrailsWebRequest.lookup().is(GrailsWebRequest.lookup(req)) and params.retained == 'filter value' (:131-132) in every hidden-method feature. Both are about reusing the filter's instance, which the thread row of service selects an active request ... already covers. If one of them ever fails, it fails in a feature named for the hidden method. I'd drop them. Going through service() instead of calling checkMultipart directly is fine.
  • UrlMappingsHandlerMappingSpec imports org.grails.web.util.GrailsApplicationAttributes twice (lines 42 and 44), and the new imports aren't in the file's order.

6. Smaller points

  • The commit messages are named after review rounds, so please squash on merge with a short message.
  • The PR description doesn't mention the params handling or the GrailsParameterMap change. That's moot if they go.

Verified as correct

  • The first commit's fix: it publishes the instance it binds when a later filter replaced the binding, so the request attribute and RequestContextHolder agree. On 7.0.x it turns the failing example app into a passing one and makes uploads behind a late filter work (1).
  • Callable threads (point 1 of #16516): a null previous binding gets its own instance and leaves the request attribute alone. a Callable keeps an active request binding after the initial dispatch returns covers it.
  • Forwards and includes through a late filter: GrailsWebRequestFilter binds a new instance on FORWARD and INCLUDE, the dispatcher reuses it, and the enclosing instance is restored afterwards. nested #dispatchType through a late filter ... covers both.
  • The example app registers the late filter the way the issue does, has no conventional results.gsp, and asserts on the HTTP response. It fails with the old lookup(request), so it's a real reproducer.

@ruthst00
ruthst00 force-pushed the fix/15819-dispatcher-grailswebrequest branch from bd43f4f to 41dfd96 Compare November 13, 2026 17:55
@ruthst00
ruthst00 changed the base branch from 8.0.x to 7.0.x November 13, 2026 17:55
@ruthst00

ruthst00 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor Author

@matrei, thanks very much for the exhaustive review, and for your patience. To keep things simple, I started with the following changes, as you requested. Then we can build from there as you see fit:

  • Retargeted the PR to 7.0.x.
  • Replaced the branch history with one commit, retaining the original fix from 4bc3f4bdc6a with compatibility adaptations.
  • New commit: 41dfd960ae. The SHA changed because it was replayed onto 7.0.x.
  • Removed all later review fixes from the PR.

Please let me know what the next step is, if any.

cc: @jamesfredley

@ruthst00
ruthst00 force-pushed the fix/15819-dispatcher-grailswebrequest branch from 41dfd96 to 1588c19 Compare November 13, 2026 18:14

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not approving. The flat and nested names still share their array. Publishing the new request drops a filter's edit of one value, so the controller sees only the wrapper's values.

// and rendering agree, while preserving the request and response wrappers seen here.
// FrameworkServlet passes null on Callable threads, which need their own instance
// without replacing the attribute still used by the dispatch.
request.setAttribute(GrailsApplicationAttributes.WEB_REQUEST, webRequest)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Request tags are raw and unchanged. A filter sets params.tags[0] to filter. A later wrapper supplies wrapped and values, and a late filter replaces the thread binding. The controller should receive filter and values. It receives wrapped and values, because the new request is published without the filter's edit. Carry that edit onto the published request.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

3 participants