Je souhaite exécuter une demande PATCH prise en charge par notre serveur pour les tests à l'aide du client Jersey. Mon code est comme ci-dessous, mais j'obtiens com.Sun.jersey.api.client.ClientHandlerException: Java.net.ProtocolException: HTTP method PATCH doesn't support output
exception. Quelqu'un peut-il me dire ce qui ne va pas avec le code ci-dessous?
String complete_url = "http://localhost:8080/api/request";
String request = "[{\"op\":\"add\", \"path\":\"/name\", \"value\":\"Hello\"}]";
DefaultClientConfig config = new DefaultClientConfig();
config.getProperties().put(URLConnectionClientHandler.PROPERTY_HTTP_URL_CONNECTION_SET_METHOD_WORKAROUND, true);
Client client = Client.create(config);
WebResource resource = client.resource(complete_url);
ClientResponse response = resource.header("Authorization", "Basic xyzabCDef")
.type(new MediaType("application", "json-patch+json"))
.method("PATCH", ClientResponse.class, request);
Voici l'exception complète,
com.Sun.jersey.api.client.ClientHandlerException: Java.net.ProtocolException: HTTP method PATCH doesn't support output
at com.Sun.jersey.client.urlconnection.URLConnectionClientHandler.handle(URLConnectionClientHandler.Java:155)
at com.Sun.jersey.api.client.Client.handle(Client.Java:652)
at com.Sun.jersey.api.client.WebResource.handle(WebResource.Java:682)
at com.Sun.jersey.api.client.WebResource.access$200(WebResource.Java:74)
at com.Sun.jersey.api.client.WebResource$Builder.method(WebResource.Java:634)
at com.acceptance.common.PatchTest.patch(PatchTest.Java:42)
at Sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at Sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.Java:39)
at Sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.Java:25)
at Java.lang.reflect.Method.invoke(Method.Java:597)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.Java:47)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.Java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.Java:44)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.Java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.Java:271)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:70)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.Java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.Java:238)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.Java:63)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.Java:236)
at org.junit.runners.ParentRunner.access$000(ParentRunner.Java:53)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.Java:229)
at org.junit.runners.ParentRunner.run(ParentRunner.Java:309)
at org.Eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.Java:50)
at org.Eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.Java:38)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:467)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.Java:683)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.Java:390)
at org.Eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.Java:197)
Caused by: Java.net.ProtocolException: HTTP method PATCH doesn't support output
at Sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.Java:1021)
at com.Sun.jersey.client.urlconnection.URLConnectionClientHandler$1$1.getOutputStream(URLConnectionClientHandler.Java:238)
at com.Sun.jersey.api.client.CommittingOutputStream.commitStream(CommittingOutputStream.Java:117)
at com.Sun.jersey.api.client.CommittingOutputStream.write(CommittingOutputStream.Java:89)
at Sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.Java:202)
at Sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.Java:272)
at Sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.Java:276)
at Sun.nio.cs.StreamEncoder.flush(StreamEncoder.Java:122)
at Java.io.OutputStreamWriter.flush(OutputStreamWriter.Java:212)
at Java.io.BufferedWriter.flush(BufferedWriter.Java:236)
at com.Sun.jersey.core.util.ReaderWriter.writeToAsString(ReaderWriter.Java:191)
at com.Sun.jersey.core.provider.AbstractMessageReaderWriterProvider.writeToAsString(AbstractMessageReaderWriterProvider.Java:128)
at com.Sun.jersey.core.impl.provider.entity.StringProvider.writeTo(StringProvider.Java:88)
at com.Sun.jersey.core.impl.provider.entity.StringProvider.writeTo(StringProvider.Java:58)
at com.Sun.jersey.api.client.RequestWriter.writeRequestEntity(RequestWriter.Java:300)
at com.Sun.jersey.client.urlconnection.URLConnectionClientHandler._invoke(URLConnectionClientHandler.Java:217)
at com.Sun.jersey.client.urlconnection.URLConnectionClientHandler.handle(URLConnectionClientHandler.Java:153)
Il s'agit d'un bogue dans l'implémentation JDK actuelle qui a été corrigé dans l'implémentation JDK8. Consultez ce lien pour plus de détails https://bugs.openjdk.Java.net/browse/JDK-715736 . Il existe un moyen de contourner cela, mais l'équipe de Jersey a décidé de ne pas y remédier https://github.com/Eclipse-ee4j/jersey/issues/1639
2 solutions auxquelles je peux penser
Pour info - juste au cas où quelqu'un rencontrerait ça dans Jersey 2, voir:
et utilisez la propriété SET_METHOD_WORKAROUND comme suit:
Client jerseyClient = ClientBuilder.newClient()
.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true)
... etc ...
Cela m'a pris une éternité pour trouver cela - je pensais pouvoir aider à court-circuiter la courbe d'apprentissage pour les autres.
Si vous utilisez HttpsUrlConnection
(notez le 's') - puis définissez le HttpUrlConnectorProvider.SET_METHOD_WORKAROUND
ne fonctionnera pas. Continuez à lire pour une solution détaillée.
Dans mon cas, la configuration de HttpUrlConnectorProvider.SET_METHOD_WORKAROUND
property a provoqué une NoSuchFieldException
car mon HttpUrlConnection
instance était en fait de type: Sun.net.www.protocol.https.HttpsURLConnectionImpl
et c'est super: javax.net.ssl.HttpsURLConnection
(qui hérite de HttpUrlConnection
).
Ainsi, lorsque le code Jackson essaie d'obtenir le champ de méthode de mon instance de connexion super (instance de javax.net.ssl.HttpsURLConnection
) ici:
/**
* Workaround for a bug in {@code HttpURLConnection.setRequestMethod(String)}
* The implementation of Sun/Oracle is throwing a {@code ProtocolException}
* when the method is other than the HTTP/1.1 default methods. So to use {@code PROPFIND}
* and others, we must apply this workaround.
*
* See issue http://Java.net/jira/browse/JERSEY-639
*/
private static void setRequestMethodViaJreBugWorkaround(final HttpURLConnection httpURLConnection, final String method) {
try {
httpURLConnection.setRequestMethod(method); // Check whether we are running on a buggy JRE
} catch (final ProtocolException pe) {
try {
final Class<?> httpURLConnectionClass = httpURLConnection.getClass();
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws NoSuchFieldException, IllegalAccessException {
final Field methodField = httpURLConnectionClass.getSuperclass().getDeclaredField("method");
methodField.setAccessible(true);
methodField.set(httpURLConnection, method);
return null;
}
});
} catch (final PrivilegedActionException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw new RuntimeException(cause);
}
}
}
}
Nous obtenons un NoSuchFieldException
indiquant qu'un champ nommé method
n'existe pas (puisque getDeclaredFields () apporte tous les champs, quelle que soit leur accessibilité mais uniquement pour la classe actuelle, pas pour les classes de base qui la classe actuelle peut hériter de).
J'ai donc regardé le code HttpUrlConnection de Java et j'ai vu que les méthodes autorisées sont spécifiées par une chaîne private static []:
/* Adding PATCH to the valid HTTP methods */
private static final String[] methods = {
"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
};
La solution était de changer ce tableau de méthodes en utilisant la réflexion:
try {
Field methodsField = HttpURLConnection.class.getDeclaredField("methods");
methodsField.setAccessible(true);
// get the methods field modifiers
Field modifiersField = Field.class.getDeclaredField("modifiers");
// bypass the "private" modifier
modifiersField.setAccessible(true);
// remove the "final" modifier
modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);
/* valid HTTP methods */
String[] methods = {
"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "PATCH"
};
// set the new methods - including patch
methodsField.set(null, methods);
} catch (SecurityException | IllegalArgumentException | IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
Le champ des méthodes étant statique, la modification de sa valeur fonctionne pour toute instance concrète qui étend HttpUrlConnection
y compris HttpsUrlConnection
.
Note latérale: Je préférerais Java pour ajouter la méthode PATCH au JDK ou à partir de Jackson pour effectuer la recherche de champ dans toute la hiérarchie dans leur solution de contournement.
Quoi qu'il en soit, j'espère que cette solution vous fera gagner du temps.
La réponse simple serait:
Dépendances
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey.core/jersey-client -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>2.27</version>
</dependency>
Besoin d'ajouter HttpUrlConnectorProvider.SET_METHOD_WORKAROUND
= true
Client client = ClientBuilder.newClient(clientConfig).property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
Demande
client.target("base_url").path("end_point").request().headers("your_headers")
.build("PATCH", Entity.entity("body_you_want_to_pass", "content-type"))
.invoke();