Repository navigation
Conversation
HttpMethodInterceptor calls HttpMethod.parse() with the request method for any action implementing HttpMethodAware. HttpMethod is an enum whose parse() is valueOf(method.toUpperCase()) with no fallback, and the interceptor has no try/catch, so any method outside its nine constants - PROPFIND, MKCOL, REPORT, or any invented token - raises IllegalArgumentException before the allow-checks run and reaches the client as a 500. Add HttpMethod.parseOrNull(), a total function returning null for an unrecognised method, and use it in the interceptor, leaving the method unset so the request falls through to the existing isAllowed() check. With http-method configured that answers 405 with an Allow header, which is the response the code already produces for a disallowed method. Both parse() and parseOrNull() also use toUpperCase(Locale.ROOT); the bare call is locale-sensitive, and XmlDocConfigurationProvider already uses Locale.ROOT for the same lookup. Tests added to HttpMethodTest.
lukaszlenart
left a comment
There was a problem hiding this comment.
Thanks for the fix and the clear write-up. I reproduced the bug with an HttpMethodAware action and a PROPFIND request: IllegalArgumentException on main, 405 with Allow: POST on this branch. This is an ordinary bug and the public PR is the right place for it.
Before we go further, please create a Jira ticket at https://issues.apache.org/jira/projects/WW (type Bug, component Core Interceptors) and retitle the PR and the commit as WW-XXXX Handle an unknown HTTP method instead of raising IllegalArgumentException. If you would rather not create an account, say so and I will file it and give you the key.
The inline comments cover the rest. The one I would not merge without is the interceptor-level regression test.
| ((HttpMethodAware) (action)).setMethod(HttpMethod.parse(request.getMethod())); | ||
| HttpMethod requestedHttpMethod = HttpMethod.parseOrNull(request.getMethod()); | ||
| if (requestedHttpMethod != null) { | ||
| ((HttpMethodAware) (action)).setMethod(requestedHttpMethod); |
There was a problem hiding this comment.
The four new tests exercise the enum directly, so none of them reach this line. I reverted this hunk locally and HttpMethodTest + HttpMethodInterceptorTest still pass. The existing testConfigRejectsUnknownRequestMethod uses TestAction, which is not HttpMethodAware, so it never hit the old parse() call.
Please add a test to HttpMethodInterceptorTest that drives the fixed path, along the lines of:
public void testHttpMethodAwareActionWithUnknownRequestMethod() throws Exception {
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action, actionConfig());
MockHttpServletResponse response = prepareRequest("PROPFIND");
String resultName = interceptor.intercept(invocation);
assertEquals(Action.NONE, resultName);
assertEquals(HttpServletResponse.SC_METHOD_NOT_ALLOWED, response.getStatus());
assertEquals("POST", response.getHeader("Allow"));
assertNull(action.getHttpMethod());
}| action, HttpMethodAware.class.getSimpleName(), request.getMethod()); | ||
| ((HttpMethodAware) (action)).setMethod(HttpMethod.parse(request.getMethod())); | ||
| HttpMethod requestedHttpMethod = HttpMethod.parseOrNull(request.getMethod()); | ||
| if (requestedHttpMethod != null) { |
There was a problem hiding this comment.
Skipping the call changes the HttpMethodAware contract: its Javadoc says the action is notified about the request method, and an action instance that is reused (a Spring singleton, a custom ObjectFactory) would keep the previous request's method. I would call setMethod(requestedHttpMethod) unconditionally and add a line to HttpMethodAware#setMethod saying null means the request method is not one of the enum constants.
The debug line just above also still says "setting request method: PROPFIND" when nothing is set; worth adjusting once the null is passed through.
| } | ||
| } | ||
|
|
||
| public static HttpMethod parse(String httpRequestMethod) { |
There was a problem hiding this comment.
After this change parse() has no caller in src/main: XmlDocConfigurationProvider.buildHttpMethods calls valueOf(value.toUpperCase(Locale.ROOT)) directly. The new shouldStillRejectUnknownMethodInConfiguration test describes parse() as the configuration path, which it currently is not.
Simplest fix: have buildHttpMethods call HttpMethod.parse(value) so there is one lookup and one locale rule, and the test's claim becomes true.
| return null; | ||
| } | ||
|
|
||
| try { |
There was a problem hiding this comment.
Minor: catching IllegalArgumentException on a client-driven path uses the exception as control flow. The interceptor already matches by name with equalsIgnoreCase in isAllowed, so a lookup in the same style keeps the two consistent and drops the locale question:
return Arrays.stream(values())
.filter(method -> method.name().equalsIgnoreCase(httpRequestMethod))
.findFirst()
.orElse(null);| CONNECT, | ||
| PATCH; | ||
|
|
||
| /** |
There was a problem hiding this comment.
Minor: the second paragraph is the rationale from the PR description, which is where it is best kept. A one-liner is enough here: @return the matching method, or null when the request method is not one of the constants.
| assertNull(httpMethod); | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
Minor: JUnit 4.13 has assertThrows, which reads as the intent and fails with a clear message:
assertThrows(IllegalArgumentException.class, () -> HttpMethod.parse("PROPFIND"));
An unknown HTTP method reaches
HttpMethodInterceptorand produces a 500.HttpMethod.parse()isvalueOf(method.toUpperCase())with no fallback, and the interceptor calls it with the raw request method for anyHttpMethodAwareaction — before the allow-checks, and with no try/catch:The enum has nine constants, so
PROPFIND,MKCOL,REPORTor any invented token raisesIllegalArgumentExceptioninstead of being treated as "not allowed". Whenhttp-methodis configured the code already knows how to answer that case — 405 with anAllowheader — it just never gets there.HttpMethod.parseOrNull()returnsnullfor an unrecognised method; the interceptor leaves the method unset and falls through to the existingisAllowed()check. Bothparse()andparseOrNull()usetoUpperCase(Locale.ROOT)— the bare call is locale-sensitive, andXmlDocConfigurationProvideralready usesLocale.ROOTfor the same lookup.Repro: any action with
http-methodset, requestPROPFIND /that-action→ 500 before, 405 +Allowafter.Tests: four added to
HttpMethodTest.mvn -pl core -Dtest=HttpMethodTest test→ BUILD SUCCESS, 6 tests, 0 failures (JDK 21).Two process notes, so you can redirect me:
WW-XXXXticket for this yet, and the contributing guide requires one. Tell me the key to use and I will update the title and commit message to match.SECURITY.md. If you would rather it went through security@struts.apache.org, close this and I will send it there instead.