Skip to content

Commit e5829c4

Browse files
cziberpvclaude
andcommitted
fix(java/feign): handle binary response types in ApiResponseDecoder
The Feign library's ApiResponseDecoder routes all responses through JacksonDecoder, including binary ones (File, byte[], InputStream). This causes JsonParseException when an endpoint returns non-JSON content (e.g. PDF, ZIP, images). Add binary type detection and handling before delegating to JacksonDecoder. This applies to both direct return types and ApiResponse<T> wrappers. Consistent with the native library fix in #21346. Closes #2486 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2ee50ce commit e5829c4

4 files changed

Lines changed: 248 additions & 20 deletions

File tree

modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiResponseDecoder.mustache

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,90 @@ import feign.Response;
77
import feign.Types;
88
import feign.jackson.JacksonDecoder;
99

10+
import java.io.File;
1011
import java.io.IOException;
12+
import java.io.InputStream;
1113
import java.lang.reflect.ParameterizedType;
1214
import java.lang.reflect.Type;
15+
import java.nio.file.Files;
16+
import java.nio.file.StandardCopyOption;
1317
import java.util.Collection;
1418
import java.util.Collections;
1519
import java.util.Map;
20+
import java.util.regex.Matcher;
21+
import java.util.regex.Pattern;
1622

1723
import {{modelPackage}}.ApiResponse;
1824

1925
public class ApiResponseDecoder extends JacksonDecoder {
2026
27+
private static final Pattern FILENAME_PATTERN =
28+
Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
29+
2130
public ApiResponseDecoder(ObjectMapper mapper) {
2231
super(mapper);
2332
}
2433

2534
@Override
2635
public Object decode(Response response, Type type) throws IOException {
27-
//Detects if the type is an instance of the parameterized class ApiResponse
2836
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
29-
//The ApiResponse class has a single type parameter, the Dto class itself
3037
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
31-
Object body = super.decode(response, responseBodyType);
38+
Object body = isBinaryType(responseBodyType)
39+
? decodeBinary(response, responseBodyType)
40+
: super.decode(response, responseBodyType);
3241
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
3342
return new ApiResponse<>(response.status(), responseHeaders, body);
43+
}
44+
45+
if (isBinaryType(type)) {
46+
return decodeBinary(response, type);
47+
}
48+
49+
return super.decode(response, type);
50+
}
51+
52+
private boolean isBinaryType(Type type) {
53+
Class<?> raw = Types.getRawType(type);
54+
return File.class.isAssignableFrom(raw)
55+
|| byte[].class.isAssignableFrom(raw)
56+
|| InputStream.class.isAssignableFrom(raw);
57+
}
58+
59+
private Object decodeBinary(Response response, Type type) throws IOException {
60+
Class<?> raw = Types.getRawType(type);
61+
if (byte[].class.isAssignableFrom(raw)) {
62+
return response.body().asInputStream().readAllBytes();
63+
}
64+
if (InputStream.class.isAssignableFrom(raw)) {
65+
return response.body().asInputStream();
66+
}
67+
return downloadToTempFile(response);
68+
}
69+
70+
private File downloadToTempFile(Response response) throws IOException {
71+
String filename = extractFilename(response);
72+
File file;
73+
if (filename != null) {
74+
java.nio.file.Path tempDir = Files.createTempDirectory("feign-download");
75+
file = Files.createFile(tempDir.resolve(filename)).toFile();
76+
tempDir.toFile().deleteOnExit();
3477
} else {
35-
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
36-
return super.decode(response, type);
78+
file = Files.createTempFile("download-", "").toFile();
79+
}
80+
file.deleteOnExit();
81+
try (InputStream is = response.body().asInputStream()) {
82+
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
83+
}
84+
return file;
85+
}
86+
87+
private String extractFilename(Response response) {
88+
Collection<String> dispositions = response.headers().get("Content-Disposition");
89+
if (dispositions == null) return null;
90+
for (String disposition : dispositions) {
91+
Matcher m = FILENAME_PATTERN.matcher(disposition);
92+
if (m.find()) return m.group(1);
3793
}
94+
return null;
3895
}
3996
}

samples/client/petstore/java/feign-hc5/src/main/java/org/openapitools/client/ApiResponseDecoder.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,90 @@
1818
import feign.Types;
1919
import feign.jackson.JacksonDecoder;
2020

21+
import java.io.File;
2122
import java.io.IOException;
23+
import java.io.InputStream;
2224
import java.lang.reflect.ParameterizedType;
2325
import java.lang.reflect.Type;
26+
import java.nio.file.Files;
27+
import java.nio.file.StandardCopyOption;
2428
import java.util.Collection;
2529
import java.util.Collections;
2630
import java.util.Map;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
2733

2834
import org.openapitools.client.model.ApiResponse;
2935

3036
public class ApiResponseDecoder extends JacksonDecoder {
3137

38+
private static final Pattern FILENAME_PATTERN =
39+
Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
40+
3241
public ApiResponseDecoder(ObjectMapper mapper) {
3342
super(mapper);
3443
}
3544

3645
@Override
3746
public Object decode(Response response, Type type) throws IOException {
38-
//Detects if the type is an instance of the parameterized class ApiResponse
3947
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
40-
//The ApiResponse class has a single type parameter, the Dto class itself
4148
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
42-
Object body = super.decode(response, responseBodyType);
49+
Object body = isBinaryType(responseBodyType)
50+
? decodeBinary(response, responseBodyType)
51+
: super.decode(response, responseBodyType);
4352
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
4453
return new ApiResponse<>(response.status(), responseHeaders, body);
54+
}
55+
56+
if (isBinaryType(type)) {
57+
return decodeBinary(response, type);
58+
}
59+
60+
return super.decode(response, type);
61+
}
62+
63+
private boolean isBinaryType(Type type) {
64+
Class<?> raw = Types.getRawType(type);
65+
return File.class.isAssignableFrom(raw)
66+
|| byte[].class.isAssignableFrom(raw)
67+
|| InputStream.class.isAssignableFrom(raw);
68+
}
69+
70+
private Object decodeBinary(Response response, Type type) throws IOException {
71+
Class<?> raw = Types.getRawType(type);
72+
if (byte[].class.isAssignableFrom(raw)) {
73+
return response.body().asInputStream().readAllBytes();
74+
}
75+
if (InputStream.class.isAssignableFrom(raw)) {
76+
return response.body().asInputStream();
77+
}
78+
return downloadToTempFile(response);
79+
}
80+
81+
private File downloadToTempFile(Response response) throws IOException {
82+
String filename = extractFilename(response);
83+
File file;
84+
if (filename != null) {
85+
java.nio.file.Path tempDir = Files.createTempDirectory("feign-download");
86+
file = Files.createFile(tempDir.resolve(filename)).toFile();
87+
tempDir.toFile().deleteOnExit();
4588
} else {
46-
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
47-
return super.decode(response, type);
89+
file = Files.createTempFile("download-", "").toFile();
90+
}
91+
file.deleteOnExit();
92+
try (InputStream is = response.body().asInputStream()) {
93+
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
94+
}
95+
return file;
96+
}
97+
98+
private String extractFilename(Response response) {
99+
Collection<String> dispositions = response.headers().get("Content-Disposition");
100+
if (dispositions == null) return null;
101+
for (String disposition : dispositions) {
102+
Matcher m = FILENAME_PATTERN.matcher(disposition);
103+
if (m.find()) return m.group(1);
48104
}
105+
return null;
49106
}
50107
}

samples/client/petstore/java/feign-no-nullable/src/main/java/org/openapitools/client/ApiResponseDecoder.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,90 @@
1818
import feign.Types;
1919
import feign.jackson.JacksonDecoder;
2020

21+
import java.io.File;
2122
import java.io.IOException;
23+
import java.io.InputStream;
2224
import java.lang.reflect.ParameterizedType;
2325
import java.lang.reflect.Type;
26+
import java.nio.file.Files;
27+
import java.nio.file.StandardCopyOption;
2428
import java.util.Collection;
2529
import java.util.Collections;
2630
import java.util.Map;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
2733

2834
import org.openapitools.client.model.ApiResponse;
2935

3036
public class ApiResponseDecoder extends JacksonDecoder {
3137

38+
private static final Pattern FILENAME_PATTERN =
39+
Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
40+
3241
public ApiResponseDecoder(ObjectMapper mapper) {
3342
super(mapper);
3443
}
3544

3645
@Override
3746
public Object decode(Response response, Type type) throws IOException {
38-
//Detects if the type is an instance of the parameterized class ApiResponse
3947
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
40-
//The ApiResponse class has a single type parameter, the Dto class itself
4148
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
42-
Object body = super.decode(response, responseBodyType);
49+
Object body = isBinaryType(responseBodyType)
50+
? decodeBinary(response, responseBodyType)
51+
: super.decode(response, responseBodyType);
4352
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
4453
return new ApiResponse<>(response.status(), responseHeaders, body);
54+
}
55+
56+
if (isBinaryType(type)) {
57+
return decodeBinary(response, type);
58+
}
59+
60+
return super.decode(response, type);
61+
}
62+
63+
private boolean isBinaryType(Type type) {
64+
Class<?> raw = Types.getRawType(type);
65+
return File.class.isAssignableFrom(raw)
66+
|| byte[].class.isAssignableFrom(raw)
67+
|| InputStream.class.isAssignableFrom(raw);
68+
}
69+
70+
private Object decodeBinary(Response response, Type type) throws IOException {
71+
Class<?> raw = Types.getRawType(type);
72+
if (byte[].class.isAssignableFrom(raw)) {
73+
return response.body().asInputStream().readAllBytes();
74+
}
75+
if (InputStream.class.isAssignableFrom(raw)) {
76+
return response.body().asInputStream();
77+
}
78+
return downloadToTempFile(response);
79+
}
80+
81+
private File downloadToTempFile(Response response) throws IOException {
82+
String filename = extractFilename(response);
83+
File file;
84+
if (filename != null) {
85+
java.nio.file.Path tempDir = Files.createTempDirectory("feign-download");
86+
file = Files.createFile(tempDir.resolve(filename)).toFile();
87+
tempDir.toFile().deleteOnExit();
4588
} else {
46-
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
47-
return super.decode(response, type);
89+
file = Files.createTempFile("download-", "").toFile();
90+
}
91+
file.deleteOnExit();
92+
try (InputStream is = response.body().asInputStream()) {
93+
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
94+
}
95+
return file;
96+
}
97+
98+
private String extractFilename(Response response) {
99+
Collection<String> dispositions = response.headers().get("Content-Disposition");
100+
if (dispositions == null) return null;
101+
for (String disposition : dispositions) {
102+
Matcher m = FILENAME_PATTERN.matcher(disposition);
103+
if (m.find()) return m.group(1);
48104
}
105+
return null;
49106
}
50107
}

samples/client/petstore/java/feign/src/main/java/org/openapitools/client/ApiResponseDecoder.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,90 @@
1818
import feign.Types;
1919
import feign.jackson.JacksonDecoder;
2020

21+
import java.io.File;
2122
import java.io.IOException;
23+
import java.io.InputStream;
2224
import java.lang.reflect.ParameterizedType;
2325
import java.lang.reflect.Type;
26+
import java.nio.file.Files;
27+
import java.nio.file.StandardCopyOption;
2428
import java.util.Collection;
2529
import java.util.Collections;
2630
import java.util.Map;
31+
import java.util.regex.Matcher;
32+
import java.util.regex.Pattern;
2733

2834
import org.openapitools.client.model.ApiResponse;
2935

3036
public class ApiResponseDecoder extends JacksonDecoder {
3137

38+
private static final Pattern FILENAME_PATTERN =
39+
Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?");
40+
3241
public ApiResponseDecoder(ObjectMapper mapper) {
3342
super(mapper);
3443
}
3544

3645
@Override
3746
public Object decode(Response response, Type type) throws IOException {
38-
//Detects if the type is an instance of the parameterized class ApiResponse
3947
if (type instanceof ParameterizedType && Types.getRawType(type).isAssignableFrom(ApiResponse.class)) {
40-
//The ApiResponse class has a single type parameter, the Dto class itself
4148
Type responseBodyType = ((ParameterizedType) type).getActualTypeArguments()[0];
42-
Object body = super.decode(response, responseBodyType);
49+
Object body = isBinaryType(responseBodyType)
50+
? decodeBinary(response, responseBodyType)
51+
: super.decode(response, responseBodyType);
4352
Map<String, Collection<String>> responseHeaders = Collections.unmodifiableMap(response.headers());
4453
return new ApiResponse<>(response.status(), responseHeaders, body);
54+
}
55+
56+
if (isBinaryType(type)) {
57+
return decodeBinary(response, type);
58+
}
59+
60+
return super.decode(response, type);
61+
}
62+
63+
private boolean isBinaryType(Type type) {
64+
Class<?> raw = Types.getRawType(type);
65+
return File.class.isAssignableFrom(raw)
66+
|| byte[].class.isAssignableFrom(raw)
67+
|| InputStream.class.isAssignableFrom(raw);
68+
}
69+
70+
private Object decodeBinary(Response response, Type type) throws IOException {
71+
Class<?> raw = Types.getRawType(type);
72+
if (byte[].class.isAssignableFrom(raw)) {
73+
return response.body().asInputStream().readAllBytes();
74+
}
75+
if (InputStream.class.isAssignableFrom(raw)) {
76+
return response.body().asInputStream();
77+
}
78+
return downloadToTempFile(response);
79+
}
80+
81+
private File downloadToTempFile(Response response) throws IOException {
82+
String filename = extractFilename(response);
83+
File file;
84+
if (filename != null) {
85+
java.nio.file.Path tempDir = Files.createTempDirectory("feign-download");
86+
file = Files.createFile(tempDir.resolve(filename)).toFile();
87+
tempDir.toFile().deleteOnExit();
4588
} else {
46-
//The response is not encapsulated in the ApiResponse, decode the Dto as normal
47-
return super.decode(response, type);
89+
file = Files.createTempFile("download-", "").toFile();
90+
}
91+
file.deleteOnExit();
92+
try (InputStream is = response.body().asInputStream()) {
93+
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
94+
}
95+
return file;
96+
}
97+
98+
private String extractFilename(Response response) {
99+
Collection<String> dispositions = response.headers().get("Content-Disposition");
100+
if (dispositions == null) return null;
101+
for (String disposition : dispositions) {
102+
Matcher m = FILENAME_PATTERN.matcher(disposition);
103+
if (m.find()) return m.group(1);
48104
}
105+
return null;
49106
}
50107
}

0 commit comments

Comments
 (0)