web-dev-qa-db-fra.com

Demande PATCH à l'aide de Jersey Client

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)
17
nilesh

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

  1. utiliser Apache Http Client qui prend en charge la méthode HttpPatch
  2. utilisez Jersey Client PostReplaceFilter mais le code du conteneur doit être modifié et inclure l'en-tête X-HTTP-Method-Override avec la valeur PATCH lors de la création d'une demande de publication. Se référer à http://zcox.wordpress.com/2009/06/17/override-the-http-request-method-in-jersey/ ]
11
Abhijeet Kushe

Pour info - juste au cas où quelqu'un rencontrerait ça dans Jersey 2, voir:

https://jersey.github.io/apidocs/latest/jersey/org/glassfish/jersey/client/HttpUrlConnectorProvider.html

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.

46
Kevin Day

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_WORKAROUNDproperty 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.

11
Daniel L.

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();
1
NarendraR