Skip to content

Commit 83e0e16

Browse files
committed
Refine encoding/decoding exception handling
Starting with removing a package cycle on the use of ResponseStatusException in the codec package, this commit generally refines codec exception handling. The new [Encoding|Decoding]Exception mirror the existing HttpMessageNot[Readable|Writable]Exception and are used similarly especially to differentiate betwen 400 and 500 errors when parsing server request body content. The commit also aligns some of the exception handling of JSON and XML on the WebFlux side with that on the Spring MVC side. Issue: SPR-15516
1 parent d7e54ce commit 83e0e16

File tree

12 files changed

+147
-87
lines changed

12 files changed

+147
-87
lines changed

spring-core/src/main/java/org/springframework/core/codec/CodecException.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -19,18 +19,29 @@
1919
import org.springframework.core.NestedRuntimeException;
2020

2121
/**
22-
* Codec related exception, usually used as a wrapper for a cause exception.
22+
* General error that indicates a problem while encoding and decoding to and
23+
* from an Object stream.
2324
*
2425
* @author Sebastien Deleuze
26+
* @author Rossen Stoyanchev
2527
* @since 5.0
2628
*/
2729
@SuppressWarnings("serial")
2830
public class CodecException extends NestedRuntimeException {
2931

32+
/**
33+
* Create a new CodecException.
34+
* @param msg the detail message
35+
*/
3036
public CodecException(String msg) {
3137
super(msg);
3238
}
3339

40+
/**
41+
* Create a new CodecException.
42+
* @param msg the detail message
43+
* @param cause root cause for the exception, if any
44+
*/
3445
public CodecException(String msg, Throwable cause) {
3546
super(msg, cause);
3647
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.core.codec;
17+
18+
/**
19+
* Indicates an issue with decoding the input stream with the focus on indicating
20+
* a content issue such as a parse failure. As opposed to a more general I/O
21+
* errors, illegal state, or a {@link CodecException} such as a configuration
22+
* issue that a {@link Decoder} may choose to raise.
23+
*
24+
* <p>For example in a web application, a server side {@code DecodingException}
25+
* would translate to a response with a 400 (bad input) status while
26+
* {@code CodecException} would translate to 500 (server error) status.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 5.0
30+
* @see Decoder
31+
*/
32+
@SuppressWarnings("serial")
33+
public class DecodingException extends CodecException {
34+
35+
/**
36+
* Create a new DecodingException.
37+
* @param msg the detail message
38+
*/
39+
public DecodingException(String msg) {
40+
super(msg);
41+
}
42+
43+
/**
44+
* Create a new DecodingException.
45+
* @param msg the detail message
46+
* @param cause root cause for the exception, if any
47+
*/
48+
public DecodingException(String msg, Throwable cause) {
49+
super(msg, cause);
50+
}
51+
52+
}

spring-core/src/main/java/org/springframework/core/codec/InternalCodecException.java renamed to spring-core/src/main/java/org/springframework/core/codec/EncodingException.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,35 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package org.springframework.core.codec;
1817

1918
/**
20-
* Codec exception suitable for internal errors, like those not related to invalid data. It can be used to make sure
21-
* such error will produce a 5xx status code and not a 4xx one when reading HTTP messages for example.
19+
* Indicates an issue with encoding the input Object stream with a focus on
20+
* indicating the Objects cannot be encoded. As opposed to a more general
21+
* {@link CodecException} such as a configuration issue that an {@link Encoder}
22+
* may also choose to raise.
2223
*
23-
* @author Sebastien Deleuze
24+
* @author Rossen Stoyanchev
2425
* @since 5.0
26+
* @see Encoder
2527
*/
2628
@SuppressWarnings("serial")
27-
public class InternalCodecException extends CodecException {
29+
public class EncodingException extends CodecException {
2830

29-
public InternalCodecException(String msg) {
31+
/**
32+
* Create a new EncodingException.
33+
* @param msg the detail message
34+
*/
35+
public EncodingException(String msg) {
3036
super(msg);
3137
}
3238

33-
public InternalCodecException(String msg, Throwable cause) {
39+
/**
40+
* Create a new EncodingException.
41+
* @param msg the detail message
42+
* @param cause root cause for the exception, if any
43+
*/
44+
public EncodingException(String msg, Throwable cause) {
3445
super(msg, cause);
3546
}
3647

spring-web/src/main/java/org/springframework/http/codec/DecoderHttpMessageReader.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24-
import org.springframework.core.codec.InternalCodecException;
25-
import org.springframework.http.HttpStatus;
26-
import org.springframework.web.server.ResponseStatusException;
2724
import reactor.core.publisher.Flux;
2825
import reactor.core.publisher.Mono;
2926

@@ -88,36 +85,22 @@ public Flux<T> read(ResolvableType elementType, ReactiveHttpInputMessage message
8885
Map<String, Object> hints) {
8986

9087
MediaType contentType = getContentType(message);
91-
return this.decoder
92-
.decode(message.getBody(), elementType, contentType, hints)
93-
.onErrorMap(this::mapError);
88+
return this.decoder.decode(message.getBody(), elementType, contentType, hints);
9489
}
9590

9691
@Override
9792
public Mono<T> readMono(ResolvableType elementType, ReactiveHttpInputMessage message,
9893
Map<String, Object> hints) {
9994

10095
MediaType contentType = getContentType(message);
101-
return this.decoder
102-
.decodeToMono(message.getBody(), elementType, contentType, hints)
103-
.onErrorMap(this::mapError);
96+
return this.decoder.decodeToMono(message.getBody(), elementType, contentType, hints);
10497
}
10598

10699
private MediaType getContentType(HttpMessage inputMessage) {
107100
MediaType contentType = inputMessage.getHeaders().getContentType();
108101
return (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
109102
}
110103

111-
private Throwable mapError(Throwable ex) {
112-
if (ex instanceof ResponseStatusException) {
113-
return ex;
114-
}
115-
else if (ex instanceof InternalCodecException) {
116-
return new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to decode HTTP message", ex);
117-
}
118-
return new ResponseStatusException(HttpStatus.BAD_REQUEST, "Failed to decode HTTP message", ex);
119-
}
120-
121104

122105
// Server-side only...
123106

spring-web/src/main/java/org/springframework/http/codec/EncoderHttpMessageWriter.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,13 @@
2222
import java.util.Map;
2323

2424
import org.reactivestreams.Publisher;
25-
import org.springframework.http.HttpStatus;
26-
import org.springframework.web.server.ResponseStatusException;
2725
import reactor.core.publisher.Flux;
2826
import reactor.core.publisher.Mono;
2927

3028
import org.springframework.core.ResolvableType;
3129
import org.springframework.core.codec.Encoder;
3230
import org.springframework.core.io.buffer.DataBuffer;
31+
import org.springframework.core.io.buffer.DataBufferFactory;
3332
import org.springframework.http.MediaType;
3433
import org.springframework.http.ReactiveHttpOutputMessage;
3534
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -97,10 +96,9 @@ public Mono<Void> write(Publisher<? extends T> inputStream, ResolvableType eleme
9796
Map<String, Object> hints) {
9897

9998
MediaType contentType = updateContentType(message, mediaType);
99+
DataBufferFactory factory = message.bufferFactory();
100100

101-
Flux<DataBuffer> body = this.encoder
102-
.encode(inputStream, message.bufferFactory(), elementType, contentType, hints)
103-
.onErrorMap(this::mapError);
101+
Flux<DataBuffer> body = this.encoder.encode(inputStream, factory, elementType, contentType, hints);
104102

105103
return isStreamingMediaType(contentType) ?
106104
message.writeAndFlushWith(body.map(Flux::just)) :
@@ -139,13 +137,6 @@ private boolean isStreamingMediaType(MediaType contentType) {
139137
.anyMatch(contentType::isCompatibleWith);
140138
}
141139

142-
private Throwable mapError(Throwable ex) {
143-
if (ex instanceof ResponseStatusException) {
144-
return ex;
145-
}
146-
return new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to encode HTTP message", ex);
147-
}
148-
149140

150141
// Server side only...
151142

spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323

24+
import com.fasterxml.jackson.core.JsonProcessingException;
2425
import com.fasterxml.jackson.databind.JavaType;
2526
import com.fasterxml.jackson.databind.ObjectMapper;
2627
import com.fasterxml.jackson.databind.ObjectReader;
@@ -32,10 +33,10 @@
3233
import org.springframework.core.MethodParameter;
3334
import org.springframework.core.ResolvableType;
3435
import org.springframework.core.codec.CodecException;
36+
import org.springframework.core.codec.DecodingException;
3537
import org.springframework.core.io.buffer.DataBuffer;
3638
import org.springframework.core.io.buffer.DataBufferUtils;
3739
import org.springframework.http.codec.HttpMessageDecoder;
38-
import org.springframework.core.codec.InternalCodecException;
3940
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
4041
import org.springframework.http.server.reactive.ServerHttpRequest;
4142
import org.springframework.http.server.reactive.ServerHttpResponse;
@@ -116,10 +117,13 @@ private Flux<Object> decodeInternal(JsonObjectDecoder objectDecoder, Publisher<D
116117
return value;
117118
}
118119
catch (InvalidDefinitionException ex) {
119-
throw new InternalCodecException("Error while reading the data", ex);
120+
throw new CodecException("Type definition error: " + ex.getMessage(), ex);
121+
}
122+
catch (JsonProcessingException ex) {
123+
throw new DecodingException("JSON parse error: " + ex.getMessage(), ex);
120124
}
121125
catch (IOException ex) {
122-
throw new CodecException("Error while reading the data", ex);
126+
throw new CodecException("I/O error while reading: " + ex.getMessage(), ex);
123127
}
124128
});
125129
}

spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626

27+
import com.fasterxml.jackson.core.JsonProcessingException;
2728
import com.fasterxml.jackson.core.PrettyPrinter;
2829
import com.fasterxml.jackson.core.util.DefaultIndenter;
2930
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
@@ -39,6 +40,7 @@
3940
import org.springframework.core.MethodParameter;
4041
import org.springframework.core.ResolvableType;
4142
import org.springframework.core.codec.CodecException;
43+
import org.springframework.core.codec.EncodingException;
4244
import org.springframework.core.io.buffer.DataBuffer;
4345
import org.springframework.core.io.buffer.DataBufferFactory;
4446
import org.springframework.http.MediaType;
@@ -164,8 +166,11 @@ private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactor
164166
try {
165167
writer.writeValue(outputStream, value);
166168
}
169+
catch (JsonProcessingException ex) {
170+
throw new EncodingException("JSON encoding error: " + ex.getMessage(), ex);
171+
}
167172
catch (IOException ex) {
168-
throw new CodecException("Error while writing the data", ex);
173+
throw new CodecException("I/O error while writing: " + ex.getMessage(), ex);
169174
}
170175

171176
return buffer;

spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlDecoder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
import org.springframework.core.ResolvableType;
3939
import org.springframework.core.codec.AbstractDecoder;
40-
import org.springframework.core.codec.CodecException;
40+
import org.springframework.core.codec.DecodingException;
4141
import org.springframework.core.io.buffer.DataBuffer;
4242
import org.springframework.util.ClassUtils;
4343
import org.springframework.util.MimeType;
@@ -216,7 +216,7 @@ private Object unmarshal(List<XMLEvent> events, Class<?> outputClass) {
216216
}
217217
}
218218
catch (JAXBException ex) {
219-
throw new CodecException(ex.getMessage(), ex);
219+
throw new DecodingException(ex.getMessage(), ex);
220220
}
221221
}
222222

spring-web/src/main/java/org/springframework/http/codec/xml/Jaxb2XmlEncoder.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
import java.util.Map;
2222
import javax.xml.bind.JAXBException;
2323
import javax.xml.bind.Marshaller;
24+
import javax.xml.bind.UnmarshalException;
2425
import javax.xml.bind.annotation.XmlRootElement;
2526
import javax.xml.bind.annotation.XmlType;
2627

2728
import reactor.core.publisher.Flux;
2829

2930
import org.springframework.core.ResolvableType;
3031
import org.springframework.core.codec.AbstractSingleValueEncoder;
32+
import org.springframework.core.codec.CodecException;
33+
import org.springframework.core.codec.EncodingException;
3134
import org.springframework.core.io.buffer.DataBuffer;
3235
import org.springframework.core.io.buffer.DataBufferFactory;
3336
import org.springframework.util.ClassUtils;
@@ -73,13 +76,16 @@ protected Flux<DataBuffer> encode(Object value, DataBufferFactory dataBufferFact
7376
OutputStream outputStream = buffer.asOutputStream();
7477
Class<?> clazz = ClassUtils.getUserClass(value);
7578
Marshaller marshaller = jaxbContexts.createMarshaller(clazz);
76-
marshaller
77-
.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
79+
marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
7880
marshaller.marshal(value, outputStream);
7981
return Flux.just(buffer);
8082
}
83+
catch (UnmarshalException ex) {
84+
return Flux.error(new EncodingException(
85+
"Could not unmarshal to [" + value.getClass() + "]: " + ex.getMessage(), ex));
86+
}
8187
catch (JAXBException ex) {
82-
return Flux.error(ex);
88+
return Flux.error(new CodecException("Could not instantiate JAXBContext: " + ex.getMessage(), ex));
8389
}
8490
}
8591

0 commit comments

Comments
 (0)