-
Notifications
You must be signed in to change notification settings - Fork 14
Description
The problem occurs deserializing an error response with java versions after 1.8.
To reproduce, run the Summary.java described in the README.md but deliberately use an invalid accountId or token.
Expected output
HTTP 401 : Insufficient authorization to perform request. at com.oanda.v20.ThrowableAdapterFactory$1.read(ThrowableAdapterFactory.java:53) at com.oanda.v20.ThrowableAdapterFactory$1.read(ThrowableAdapterFactory.java:35) at com.google.gson.Gson.fromJson(Gson.java:888) at com.google.gson.Gson.fromJson(Gson.java:853) at com.google.gson.Gson.fromJson(Gson.java:802) at com.google.gson.Gson.fromJson(Gson.java:774) at com.oanda.v20.Context.execute(Context.java:302) at com.oanda.v20.account.AccountContext.summary(AccountContext.java:117) at com.oanda.v20.TestSummary.main(TestSummary.java:14)
Actual output when compiled with Java 21
java.lang.reflect.InaccessibleObjectException: Unable to make field private java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not "opens java.lang" to unnamed module @7b7fdc8 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178) at java.base/java.lang.reflect.Field.setAccessible(Field.java:172) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:157) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:100) at com.google.gson.Gson.getAdapter(Gson.java:423) at com.google.gson.Gson.fromJson(Gson.java:887) at com.google.gson.Gson.fromJson(Gson.java:853) at com.google.gson.Gson.fromJson(Gson.java:802) at com.google.gson.Gson.fromJson(Gson.java:774) at com.oanda.v20.Context.execute(Context.java:303) at com.oanda.v20.account.AccountContext.summary(AccountContext.java:117) at Summary.main(Summary.java:13)
This can be fixed with by adding this to the GsonBuilder in Context.java
.registerTypeAdapterFactory(ThrowableAdapterFactory.INSTANCE)
Here is the ThrowableAdapterFactory
package com.oanda.v20;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
/**
* This is the fix for gson 2.10.1:
* <p>
* com.google.gson.JsonIOException:
* Failed making field 'java.lang.Throwable#detailMessage' accessible; either increase
* its visibility or write a custom TypeAdapter for its declaring type.
* <p>
* with gson 2.8.2 the error is
* <p>java.lang.reflect.InaccessibleObjectException: Unable to make field private
* java.lang.String java.lang.Throwable.detailMessage accessible: module java.base does not
* "opens java.lang" to unnamed module @7b7fdc8</p>
* see <a href="https://github.com/google/gson/issues/2352">gson/issues/2352</a>
*/
class ThrowableAdapterFactory implements TypeAdapterFactory {
private ThrowableAdapterFactory() {}
public static final ThrowableAdapterFactory INSTANCE = new ThrowableAdapterFactory();
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// Only handles Throwable and subclasses; let other factories handle any other type
if (!Throwable.class.isAssignableFrom(type.getRawType())) {
return null;
}
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new TypeAdapter<Throwable>() {
@Override
public Throwable read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String errorMessage = "There was an error message in the response.";
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.equals("errorMessage")) {
errorMessage = in.nextString();
} else {
in.skipValue();
}
}
in.endObject();
RequestException requestException = new RequestException();
requestException.errorMessage = errorMessage;
return requestException;
}
@Override
public void write(JsonWriter out, Throwable value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
// Include exception type name to give more context; for example NullPointerException might
// not have a message
out.name("type");
out.value(value.getClass().getSimpleName());
out.name("message");
out.value(value.getMessage());
Throwable cause = value.getCause();
if (cause != null) {
out.name("cause");
write(out, cause);
}
Throwable[] suppressedArray = value.getSuppressed();
if (suppressedArray.length > 0) {
out.name("suppressed");
out.beginArray();
for (Throwable suppressed : suppressedArray) {
write(out, suppressed);
}
out.endArray();
}
out.endObject();
}
};
return adapter;
}
}