diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java index a5f271352d28..5f212ce915d3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -129,6 +129,12 @@ static AppendableByteArray get(Charset charset) { result = new AppendableByteArray(charset); cache.set(new SoftReference<>(result)); } + else { + // The cached instance is reused, so it must be clean before it is + // handed out again. A previous use may have been abandoned part-way, + // for example when writing the value threw, leaving content behind. + result.reset(); + } return result; } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java index 22cb350b450a..c2195ea438fa 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AppendableByteArrayTests.java @@ -68,6 +68,18 @@ void writeUsingCache() throws IOException { assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string)); } + @Test + void getWhenPreviousUseWasAbandonedReturnsCleanInstance() throws IOException { + AppendableByteArray abandoned = AppendableByteArray.get(StandardCharsets.UTF_8); + abandoned.append("partial content"); + // The instance is never converted to a byte array, as happens when writing + // the value throws part-way through. + AppendableByteArray reused = AppendableByteArray.get(StandardCharsets.UTF_8); + assertThat(reused).isSameAs(abandoned); + reused.append("clean"); + assertThat(reused.toByteArray()).isEqualTo("clean".getBytes(StandardCharsets.UTF_8)); + } + private void assertByteArray(Charset charset, ThrowingConsumer action) throws Exception { assertByteArray(4, 4, charset, action); } diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java index aa3ea133f04c..6d7bfbc1a602 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/WritableJsonTests.java @@ -66,6 +66,17 @@ void toByteArrayReturnsByteArray() { assertThat(writable.toByteArray()).isEqualTo("{}".getBytes()); } + @Test + void toByteArrayWhenPreviousWriteFailedDoesNotIncludePartialContent() { + WritableJson failing = (out) -> { + out.append("{\"partial\":"); + throw new IllegalStateException("bad"); + }; + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(failing::toByteArray); + WritableJson healthy = (out) -> out.append("{\"ok\":true}"); + assertThat(healthy.toByteArray()).isEqualTo("{\"ok\":true}".getBytes(StandardCharsets.UTF_8)); + } + @Test void toResourceWritesJson() throws Exception { File file = new File(this.temp, "out.json");