Skip to content

ServerHttpRequest content-type cannot be mutated #26615

Closed
@Thorn1089

Description

@Thorn1089

Affects: 5.3.3
Library: spring-web

The ReadOnlyHttpHeaders type wraps an existing (mutable) MultiValueMap. That map can be updated independently without going through the ReadOnlyHttpHeaders interface. This makes it inappropriate to cache the Accept and Content-Type headers.

For example: I am working on a Spring Cloud Gateway project. Because the multipart/form-data parser does not correctly handle quoted boundary values (a separate issue) I tried to write both a GlobalFilter and a WebFilter that would unwrap the boundary value before I attempt to use the built-in decoders for form data. This doesn't work, though, because the original quoted value is cached in a ReadOnlyHttpHeaders instance, even though its backing MultiValueMap was updated by my filter.

See an example snippet below:

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        var headers = exchange.getRequest().getHeaders();
        if(headers.getContentType().isCompatibleWith(MediaType.MULTIPART_FORM_DATA)) {
            LOG.trace("Examining multipart/form-data request");
            // Check the boundary value and rewrite it if necessary
            return chain.filter(exchange.mutate().request(req -> req.headers(original -> {
                var contentTypeValue = original.getFirst("Content-Type");
                LOG.trace("Original Content-Type header:" + contentTypeValue);
                var matcher = BOUNDARY.matcher(contentTypeValue);
                var found = matcher.find();
                if(!found) {
                    throw new IllegalStateException("A Content-Type header must specify a boundary for a multipart/form-data request (" + contentTypeValue + ")");
                }
                var boundary = matcher.group("boundary");
                if(boundary.startsWith("\"") && boundary.endsWith("\"")) {
                    //original.setContentType(new MediaType("multipart/form-data; boundary=" + boundary.substring(1, boundary.length() - 1)));
                    original.set("Content-Type", "multipart/form-data; boundary=" + boundary.substring(1, boundary.length() - 1));
                }
                LOG.trace("Modified Content-Type header:" + original.getContentType());
            })).build());
        }
        
        return chain.filter(exchange);
    }

The headers passed to the builder method are mutable and are intended to allow for this use case -- great! But then when I get to the actual boundary detection code in the multipart codec shipped with Spring Cloud Gateway, it does the following:

	@Nullable
	private static byte[] boundary(HttpMessage message) {
		MediaType contentType = message.getHeaders().getContentType();
		if (contentType != null) {
			String boundary = contentType.getParameter("boundary");
			if (boundary != null) {
				return boundary.getBytes(StandardCharsets.ISO_8859_1);
			}
		}
		return null;
	}

That HttpMessage#getHeaders() returns a ReadOnlyHttpHeaders instance, which someone else has already called getContentType() on before my filter ran. So I'm stuck with the 'broken' value of the boundary and my code dies. I have no other way to rewrite that request short of running a whole separate gateway instance in front of my actual gateway to rewrite that header and forward it.

It's not obvious why those two headers require caching. There are no comments describing reasons why they are particularly expensive to compute, for example.

Metadata

Metadata

Assignees

Labels

in: webIssues in web modules (web, webmvc, webflux, websocket)status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions