Skip to content

Commit 008a78d

Browse files
committed
Handle invalid media types as client errors
Closes gh-1145
1 parent bd8a633 commit 008a78d

File tree

8 files changed

+137
-17
lines changed

8 files changed

+137
-17
lines changed

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/AbstractGraphQlHttpHandler.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@
3131
import org.springframework.graphql.server.WebGraphQlResponse;
3232
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
3333
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.InvalidMediaTypeException;
3435
import org.springframework.http.MediaType;
3536
import org.springframework.http.codec.CodecConfigurer;
3637
import org.springframework.lang.Nullable;
@@ -101,7 +102,15 @@ public Mono<ServerResponse> handleRequest(ServerRequest request) {
101102

102103
private Mono<SerializableGraphQlRequest> readRequest(ServerRequest serverRequest) {
103104
if (this.codecDelegate != null) {
104-
MediaType contentType = serverRequest.headers().contentType().orElse(MediaType.APPLICATION_JSON);
105+
ServerRequest.Headers headers = serverRequest.headers();
106+
MediaType contentType;
107+
try {
108+
contentType = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
109+
}
110+
catch (InvalidMediaTypeException ex) {
111+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
112+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
113+
}
105114
return this.codecDelegate.decode(serverRequest.bodyToFlux(DataBuffer.class), contentType);
106115
}
107116
else {

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,10 +22,13 @@
2222

2323
import org.springframework.graphql.server.WebGraphQlHandler;
2424
import org.springframework.graphql.server.WebGraphQlResponse;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.InvalidMediaTypeException;
2527
import org.springframework.http.MediaType;
2628
import org.springframework.http.codec.CodecConfigurer;
2729
import org.springframework.web.reactive.function.server.ServerRequest;
2830
import org.springframework.web.reactive.function.server.ServerResponse;
31+
import org.springframework.web.server.NotAcceptableStatusException;
2932

3033
/**
3134
* WebFlux.fn Handler for GraphQL over HTTP requests.
@@ -67,7 +70,16 @@ protected Mono<ServerResponse> prepareResponse(ServerRequest request, WebGraphQl
6770
}
6871

6972
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
70-
for (MediaType accepted : serverRequest.headers().accept()) {
73+
ServerRequest.Headers headers = serverRequest.headers();
74+
List<MediaType> acceptedMediaTypes;
75+
try {
76+
acceptedMediaTypes = headers.accept();
77+
}
78+
catch (InvalidMediaTypeException ex) {
79+
throw new NotAcceptableStatusException("Could not parse " +
80+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
81+
}
82+
for (MediaType accepted : acceptedMediaTypes) {
7183
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
7284
return accepted;
7385
}

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlRequestPredicates.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.http.HttpHeaders;
2626
import org.springframework.http.HttpMethod;
27+
import org.springframework.http.InvalidMediaTypeException;
2728
import org.springframework.http.MediaType;
2829
import org.springframework.http.server.PathContainer;
2930
import org.springframework.lang.Nullable;
@@ -33,6 +34,8 @@
3334
import org.springframework.web.reactive.function.server.RequestPredicate;
3435
import org.springframework.web.reactive.function.server.RouterFunctions;
3536
import org.springframework.web.reactive.function.server.ServerRequest;
37+
import org.springframework.web.server.NotAcceptableStatusException;
38+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
3639
import org.springframework.web.util.pattern.PathPattern;
3740
import org.springframework.web.util.pattern.PathPatternParser;
3841

@@ -119,7 +122,14 @@ private static boolean contentTypeMatch(ServerRequest request, List<MediaType> c
119122
return true;
120123
}
121124
ServerRequest.Headers headers = request.headers();
122-
MediaType actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
125+
MediaType actual;
126+
try {
127+
actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
128+
}
129+
catch (InvalidMediaTypeException ex) {
130+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
131+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
132+
}
123133
boolean contentTypeMatch = false;
124134
for (MediaType contentType : contentTypes) {
125135
contentTypeMatch = contentType.includes(actual);
@@ -136,7 +146,14 @@ private static boolean acceptMatch(ServerRequest request, List<MediaType> expect
136146
return true;
137147
}
138148
ServerRequest.Headers headers = request.headers();
139-
List<MediaType> acceptedMediaTypes = acceptedMediaTypes(headers);
149+
List<MediaType> acceptedMediaTypes;
150+
try {
151+
acceptedMediaTypes = acceptedMediaTypes(headers);
152+
}
153+
catch (InvalidMediaTypeException ex) {
154+
throw new NotAcceptableStatusException("Could not parse " +
155+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
156+
}
140157
boolean match = false;
141158
outer:
142159
for (MediaType acceptedMediaType : acceptedMediaTypes) {

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
3838
import org.springframework.http.HttpCookie;
3939
import org.springframework.http.HttpHeaders;
40+
import org.springframework.http.InvalidMediaTypeException;
4041
import org.springframework.http.MediaType;
4142
import org.springframework.http.converter.HttpMessageConverter;
4243
import org.springframework.http.server.ServerHttpRequest;
@@ -52,6 +53,7 @@
5253
import org.springframework.util.StringUtils;
5354
import org.springframework.web.HttpMediaTypeNotSupportedException;
5455
import org.springframework.web.server.ServerWebInputException;
56+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
5557
import org.springframework.web.servlet.ModelAndView;
5658
import org.springframework.web.servlet.function.ServerRequest;
5759
import org.springframework.web.servlet.function.ServerResponse;
@@ -146,7 +148,15 @@ private static MultiValueMap<String, HttpCookie> initCookies(ServerRequest serve
146148
private GraphQlRequest readBody(ServerRequest request) throws ServletException {
147149
try {
148150
if (this.messageConverter != null) {
149-
MediaType contentType = request.headers().contentType().orElse(MediaType.APPLICATION_JSON);
151+
ServerRequest.Headers headers = request.headers();
152+
MediaType contentType;
153+
try {
154+
contentType = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
155+
}
156+
catch (InvalidMediaTypeException ex) {
157+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
158+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
159+
}
150160
if (this.messageConverter.canRead(SerializableGraphQlRequest.class, contentType)) {
151161
ServerHttpRequest httpRequest = new ServletServerHttpRequest(request.servletRequest());
152162
return (GraphQlRequest) this.messageConverter.read(SerializableGraphQlRequest.class, httpRequest);

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,9 +25,12 @@
2525

2626
import org.springframework.graphql.server.WebGraphQlHandler;
2727
import org.springframework.graphql.server.WebGraphQlResponse;
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.InvalidMediaTypeException;
2830
import org.springframework.http.MediaType;
2931
import org.springframework.http.converter.HttpMessageConverter;
3032
import org.springframework.lang.Nullable;
33+
import org.springframework.web.server.NotAcceptableStatusException;
3134
import org.springframework.web.servlet.function.ServerRequest;
3235
import org.springframework.web.servlet.function.ServerResponse;
3336

@@ -97,7 +100,16 @@ protected ServerResponse prepareResponse(ServerRequest request, Mono<WebGraphQlR
97100
}
98101

99102
private static MediaType selectResponseMediaType(ServerRequest request) {
100-
for (MediaType mediaType : request.headers().accept()) {
103+
ServerRequest.Headers headers = request.headers();
104+
List<MediaType> acceptedMediaTypes;
105+
try {
106+
acceptedMediaTypes = headers.accept();
107+
}
108+
catch (InvalidMediaTypeException ex) {
109+
throw new NotAcceptableStatusException("Could not parse " +
110+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
111+
}
112+
for (MediaType mediaType : acceptedMediaTypes) {
101113
if (SUPPORTED_MEDIA_TYPES.contains(mediaType)) {
102114
return mediaType;
103115
}

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlRequestPredicates.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424

2525
import org.springframework.http.HttpHeaders;
2626
import org.springframework.http.HttpMethod;
27+
import org.springframework.http.InvalidMediaTypeException;
2728
import org.springframework.http.MediaType;
2829
import org.springframework.http.server.PathContainer;
2930
import org.springframework.lang.Nullable;
3031
import org.springframework.util.Assert;
3132
import org.springframework.util.MimeTypeUtils;
3233
import org.springframework.web.cors.CorsUtils;
34+
import org.springframework.web.server.NotAcceptableStatusException;
35+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
3336
import org.springframework.web.servlet.function.RequestPredicate;
3437
import org.springframework.web.servlet.function.RouterFunctions;
3538
import org.springframework.web.servlet.function.ServerRequest;
@@ -119,7 +122,14 @@ private static boolean contentTypeMatch(ServerRequest request, List<MediaType> c
119122
return true;
120123
}
121124
ServerRequest.Headers headers = request.headers();
122-
MediaType actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
125+
MediaType actual;
126+
try {
127+
actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
128+
}
129+
catch (InvalidMediaTypeException ex) {
130+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
131+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
132+
}
123133
boolean contentTypeMatch = false;
124134
for (MediaType contentType : contentTypes) {
125135
contentTypeMatch = contentType.includes(actual);
@@ -136,7 +146,14 @@ private static boolean acceptMatch(ServerRequest request, List<MediaType> expect
136146
return true;
137147
}
138148
ServerRequest.Headers headers = request.headers();
139-
List<MediaType> acceptedMediaTypes = acceptedMediaTypes(headers);
149+
List<MediaType> acceptedMediaTypes;
150+
try {
151+
acceptedMediaTypes = acceptedMediaTypes(headers);
152+
}
153+
catch (InvalidMediaTypeException ex) {
154+
throw new NotAcceptableStatusException("Could not parse " +
155+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
156+
}
140157
boolean match = false;
141158
outer:
142159
for (MediaType acceptedMediaType : acceptedMediaTypes) {

spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlRequestPredicatesTests.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919

2020
import java.util.Collections;
21+
import java.util.List;
2122

2223
import org.junit.jupiter.api.Nested;
2324
import org.junit.jupiter.api.Test;
@@ -30,10 +31,13 @@
3031
import org.springframework.web.reactive.function.server.RequestPredicate;
3132
import org.springframework.web.reactive.function.server.RouterFunctions;
3233
import org.springframework.web.reactive.function.server.ServerRequest;
34+
import org.springframework.web.server.NotAcceptableStatusException;
3335
import org.springframework.web.server.ServerWebExchange;
36+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
3437
import org.springframework.web.util.pattern.PathPatternParser;
3538

3639
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3741

3842
/**
3943
* Tests for {@link GraphQlRequestPredicates}.
@@ -92,23 +96,41 @@ void shouldMapApplicationGraphQlRequestContent() {
9296
}
9397

9498
@Test
95-
void shouldRejectRequestWithDifferentContentType() {
99+
void shouldRejectRequestWithIncompatibleContentType() {
96100
ServerWebExchange exchange = createMatchingHttpExchange()
97-
.mutate().request(req -> req.headers(headers -> headers.setContentType(MediaType.TEXT_HTML)))
101+
.mutate().request(request -> request.headers(h -> h.setContentType(MediaType.TEXT_HTML)))
98102
.build();
99103
ServerRequest serverRequest = ServerRequest.create(exchange, Collections.emptyList());
100104
assertThat(httpPredicate.test(serverRequest)).isFalse();
101105
}
102106

107+
@Test
108+
void shouldRejectRequestWithInvalidContentType() {
109+
ServerWebExchange exchange = createMatchingHttpExchange()
110+
.mutate().request(request -> request.headers(h -> h.set("Content-Type", "bogus")))
111+
.build();
112+
ServerRequest request = ServerRequest.create(exchange, Collections.emptyList());
113+
assertThatThrownBy(() -> httpPredicate.test(request)).isInstanceOf(UnsupportedMediaTypeStatusException.class);
114+
}
115+
103116
@Test
104117
void shouldRejectRequestWithIncompatibleAccept() {
105118
ServerWebExchange exchange = createMatchingHttpExchange()
106-
.mutate().request(req -> req.headers(headers -> headers.setAccept(Collections.singletonList(MediaType.TEXT_HTML))))
119+
.mutate().request(request -> request.headers(h -> h.setAccept(List.of(MediaType.TEXT_HTML))))
107120
.build();
108121
ServerRequest serverRequest = ServerRequest.create(exchange, Collections.emptyList());
109122
assertThat(httpPredicate.test(serverRequest)).isFalse();
110123
}
111124

125+
@Test
126+
void shouldRejectRequestWithInvalidAccept() {
127+
ServerWebExchange exchange = createMatchingHttpExchange()
128+
.mutate().request(request -> request.headers(h -> h.set("Accept", "bogus")))
129+
.build();
130+
ServerRequest request = ServerRequest.create(exchange, Collections.emptyList());
131+
assertThatThrownBy(() -> httpPredicate.test(request)).isInstanceOf(NotAcceptableStatusException.class);
132+
}
133+
112134
@Test
113135
void shouldSetMatchingPatternAttribute() {
114136
ServerWebExchange exchange = createMatchingHttpExchange();

spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlRequestPredicatesTests.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,22 @@
1818

1919

2020
import java.util.Collections;
21+
import java.util.List;
2122

2223
import org.junit.jupiter.api.Nested;
2324
import org.junit.jupiter.api.Test;
2425

2526
import org.springframework.http.HttpHeaders;
2627
import org.springframework.mock.web.MockHttpServletRequest;
28+
import org.springframework.web.server.NotAcceptableStatusException;
29+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
2730
import org.springframework.web.servlet.function.RequestPredicate;
2831
import org.springframework.web.servlet.function.RouterFunctions;
2932
import org.springframework.web.servlet.function.ServerRequest;
3033
import org.springframework.web.util.pattern.PathPatternParser;
3134

3235
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3337

3438
/**
3539
* Tests for {@link GraphQlRequestPredicates}.
@@ -85,13 +89,21 @@ void shouldMapApplicationGraphQlRequestContent() {
8589
}
8690

8791
@Test
88-
void shouldRejectRequestWithDifferentContentType() {
92+
void shouldRejectRequestWithIncompatibleContentType() {
8993
MockHttpServletRequest request = createMatchingHttpRequest();
9094
request.setContentType("text/xml");
9195
ServerRequest serverRequest = ServerRequest.create(request, Collections.emptyList());
9296
assertThat(httpPredicate.test(serverRequest)).isFalse();
9397
}
9498

99+
@Test // gh-1145
100+
void shouldRejectRequestWithInvalidContentType() {
101+
MockHttpServletRequest servletRequest = createMatchingHttpRequest();
102+
servletRequest.setContentType("bogus");
103+
ServerRequest request = ServerRequest.create(servletRequest, List.of());
104+
assertThatThrownBy(() -> httpPredicate.test(request)).isInstanceOf(UnsupportedMediaTypeStatusException.class);
105+
}
106+
95107
@Test
96108
void shouldRejectRequestWithIncompatibleAccept() {
97109
MockHttpServletRequest request = createMatchingHttpRequest();
@@ -101,6 +113,15 @@ void shouldRejectRequestWithIncompatibleAccept() {
101113
assertThat(httpPredicate.test(serverRequest)).isFalse();
102114
}
103115

116+
@Test
117+
void shouldRejectRequestWithInvalidAccept() {
118+
MockHttpServletRequest servletRequest = createMatchingHttpRequest();
119+
servletRequest.removeHeader("Accept");
120+
servletRequest.addHeader("Accept", "bogus");
121+
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
122+
assertThatThrownBy(() -> httpPredicate.test(request)).isInstanceOf(NotAcceptableStatusException.class);
123+
}
124+
104125
@Test
105126
void shouldSetMatchingPatternAttribute() {
106127
MockHttpServletRequest request = createMatchingHttpRequest();
@@ -168,7 +189,7 @@ void shouldRejectRequestWithDifferentPath() {
168189
}
169190

170191
@Test
171-
void shouldRejectRequestWithDifferentContentType() {
192+
void shouldRejectRequestWithIncmopatibleContentType() {
172193
MockHttpServletRequest request = createMatchingSseRequest();
173194
request.setContentType("text/xml");
174195
ServerRequest serverRequest = ServerRequest.create(request, Collections.emptyList());

0 commit comments

Comments
 (0)