HttpURLConnection Método HTTP no válido: PATCH

81

Cuando intento usar un método HTTP no estándar como PATCH con URLConnection:

    HttpURLConnection conn = (HttpURLConnection) new URL("http://example.com").openConnection();
    conn.setRequestMethod("PATCH");

Obtengo una excepción:

java.net.ProtocolException: Invalid HTTP method: PATCH
at java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:440)

El uso de una API de nivel superior como Jersey genera el mismo error. ¿Existe una solución alternativa para emitir una solicitud PATCH HTTP?

kavai77
fuente

Respuestas:

48

Sí, hay una solución para esto. Utilizar

Anulación del método X-HTTP

. Este encabezado se puede utilizar en una solicitud POST para "falsificar" otros métodos HTTP. Simplemente establezca el valor del encabezado X-HTTP-Method-Override en el método HTTP que le gustaría realizar. Entonces usa el siguiente código.

conn.setRequestProperty("X-HTTP-Method-Override", "PATCH");
conn.setRequestMethod("POST");
sagar
fuente
34
Esto solo funciona si el extremo receptor lo admite. Todavía envía un "POST" en la línea.
Joseph Jaquinta
3
Si el receptor lo soporta, entonces (para mí) es la forma más limpia de proceder.
Maxime T
4
Este método funciona cuando se usa HttpUrlConnection para llamar a la API REST de Firebase.
Andrew Kelly
1
@DuanBressan, el protocolo no debería ser un problema siempre que el servidor admita uno o ambos (aunque solo debería aceptar conexiones a HTTPS)
Alexis Wilke
3
Esta no es una respuesta válida porque no soluciona el problema en el lado de Java. El servidor tiene que permitirle usar POSTy tiene que entender el X-HTTP-Method-Overridecampo. Consulte stackoverflow.com/a/46323891/3647724 para una mejor solución real
Feirell
51

Hay muchas buenas respuestas, así que aquí está la mía (no funciona en jdk12):

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

public class SupportPatch {
    public static void main(String... args) throws IOException {
        allowMethods("PATCH");

        HttpURLConnection conn = (HttpURLConnection) new URL("http://example.com").openConnection();
        conn.setRequestMethod("PATCH");
    }

    private static void allowMethods(String... methods) {
        try {
            Field methodsField = HttpURLConnection.class.getDeclaredField("methods");

            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);

            methodsField.setAccessible(true);

            String[] oldMethods = (String[]) methodsField.get(null);
            Set<String> methodsSet = new LinkedHashSet<>(Arrays.asList(oldMethods));
            methodsSet.addAll(Arrays.asList(methods));
            String[] newMethods = methodsSet.toArray(new String[0]);

            methodsField.set(null/*static field*/, newMethods);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }
}

También usa la reflexión, pero en lugar de piratear cada objeto de conexión, estamos pirateando el campo estático de métodos HttpURLConnection # que se usa en las comprobaciones internamente.

okutane
fuente
7
Muy buena respuesta, debería ser la aceptada porque resuelve el problema real y no sugiere una solución alternativa que dependa del servidor receptor
Feirell
1
de hecho, esta solución funciona como un encanto, ya que el que usa la propiedad de anulación depende del servidor (y en mi caso no funcionó) ...
Amichai Ungar
¿Esto todavía funciona con Java 9? ¿O lo del módulo lo restringe
CLOVIS
2
Intenté eso con JDK12, pero obtuve "java.lang.NoSuchFieldException: modifiers"
Kin Cheung
33

Hay un error que no se corrige en OpenJDK para esto: https://bugs.openjdk.java.net/browse/JDK-7016595

Sin embargo, con Apache Http-Components Client 4.2+ esto es posible. Tiene una implementación de red personalizada, por lo que es posible utilizar métodos HTTP no estándar como PATCH. Incluso tiene una clase HttpPatch que admite el método de parche.

CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPatch httpPatch = new HttpPatch(new URI("http://example.com"));
CloseableHttpResponse response = httpClient.execute(httpPatch);

Coordenadas de Maven:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.2+</version>
</dependency>
kavai77
fuente
17

Si el proyecto está en Spring / Gradle ; la siguiente solución funcionará.

Para build.gradle, agregue la siguiente dependencia;

compile('org.apache.httpcomponents:httpclient:4.5.2')

Y defina el siguiente bean en su clase @SpringBootApplication dentro de com.company.project;

 @Bean
 public RestTemplate restTemplate() {
  HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
  requestFactory.setReadTimeout(600000);
  requestFactory.setConnectTimeout(600000);
  return new RestTemplate(requestFactory);
 }

Esta solución funcionó para mí.

hirosht
fuente
para Spring dev, esta es la solución más limpia: return new RestTemplate (new (HttpComponentsClientHttpRequestFactory));
John Tribe
Gracias @hirosht. La forma más limpia y sencilla de resolverlo en una aplicación basada en Spring.
Daniel Camarasa
4

Tuve la misma excepción y escribí la solución de sockets (en groovy) pero traduzco el formulario de respuesta a Java para ti:

String doInvalidHttpMethod(String method, String resource){
        Socket s = new Socket(InetAddress.getByName("google.com"), 80);
        PrintWriter pw = new PrintWriter(s.getOutputStream());
        pw.println(method +" "+resource+" HTTP/1.1");
        pw.println("User-Agent: my own");
        pw.println("Host: google.com:80");
        pw.println("Content-Type: */*");
        pw.println("Accept: */*");
        pw.println("");
        pw.flush();
        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        String t = null;
        String response = ""; 
        while((t = br.readLine()) != null){
            response += t;
        }
        br.close();
        return response;
    }

Creo que funciona en java. Debe cambiar el servidor y el número de puerto, recuerde cambiar también el encabezado del host y tal vez tenga que detectar alguna excepción.

Atentamente

moralejaSinCuentoNiProverbio
fuente
1
No es válido. El terminador de línea en HTTP se especifica como \r\n, no como lo que println()proporciona.
user207421
4

La reflexión como se describe en esta publicación y una publicación relacionada no funciona si está utilizando un HttpsURLConnectionJRE de Oracle, ¡porque sun.net.www.protocol.https.HttpsURLConnectionImplestá utilizando el methodcampo java.net.HttpURLConnectionde su DelegateHttpsURLConnection!

Entonces, una solución de trabajo completa es:

private void setRequestMethod(final HttpURLConnection c, final String value) {
    try {
        final Object target;
        if (c instanceof HttpsURLConnectionImpl) {
            final Field delegate = HttpsURLConnectionImpl.class.getDeclaredField("delegate");
            delegate.setAccessible(true);
            target = delegate.get(c);
        } else {
            target = c;
        }
        final Field f = HttpURLConnection.class.getDeclaredField("method");
        f.setAccessible(true);
        f.set(target, value);
    } catch (IllegalAccessException | NoSuchFieldException ex) {
        throw new AssertionError(ex);
    }
}
rmuller
fuente
1
Si ya está usando la reflexión, ¿por qué no agregar el método "PATCH" reescribiendo el valor de los métodos java.net.HttpURLConnection # una vez por vida de la aplicación?
okutane
Buen punto. Sin embargo, mi respuesta es solo para mostrar cómo debería funcionar la solución sugerida, no para mostrar otra solución
rmuller
@okutane, ¿podría darnos una pequeña pista de cómo podemos reescribir los métodos? porque he visto pocas publicaciones hablando de delegados en él
Coder
@Dhamayanthi He publicado una respuesta separada para ello.
okutane
¿podría compartir el enlace?
Codificador
2

Usando la respuesta:

HttpURLConnection Método HTTP no válido: PATCH

He creado una solicitud de muestra y trabajo como un encanto:

public void request(String requestURL, String authorization, JsonObject json) {

    try {

        URL url = new URL(requestURL);
        httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setRequestMethod("POST");
        httpConn.setRequestProperty("X-HTTP-Method-Override", "PATCH");
        httpConn.setRequestProperty("Content-Type", "application/json");
        httpConn.setRequestProperty("Authorization", authorization);
        httpConn.setRequestProperty("charset", "utf-8");

        DataOutputStream wr = new DataOutputStream(httpConn.getOutputStream());
        wr.writeBytes(json.toString());
        wr.flush();
        wr.close();

        httpConn.connect();

        String response = finish();

        if (response != null && !response.equals("")) {
            created = true;
        }
    } 
    catch (Exception e) {
        e.printStackTrace();
    }
}

public String finish() throws IOException {

    String response = "";

    int status = httpConn.getResponseCode();
    if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                httpConn.getInputStream()));
        String line = null;
        while ((line = reader.readLine()) != null) {
            response += line;
        }
        reader.close();
        httpConn.disconnect();
    } else {
        throw new IOException("Server returned non-OK status: " + status);
    }

    return response;
}

Espero que te ayude.

Duan Bressan
fuente
Su solución funciona si el servidor conectado acepta e interpreta el encabezado de solicitud 'X-HTTP-Method-Override'. Por lo tanto, su solución no se puede utilizar en todos los casos.
Emmanuel Devaux
2

Para cualquiera que use Spring restTemplate y busque una respuesta detallada.

Enfrentará el problema si está utilizando SimpleClientHttpRequestFactory como ClientHttpRequestFactory de restTemplate.

Desde java.net.HttpURLConnection:

/* valid HTTP methods */
private static final String[] methods = {
    "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
};

Como PATCH no es una operación compatible, esta línea de código de la misma clase se ejecutará:

throw new ProtocolException("Invalid HTTP method: " + method);

Terminé usando lo mismo que sugirió @hirosht en su respuesta .

Johnu
fuente
1

Otra solución de truco sucio es la reflexión:

private void setVerb(HttpURLConnection cn, String verb) throws IOException {

  switch (verb) {
    case "GET":
    case "POST":
    case "HEAD":
    case "OPTIONS":
    case "PUT":
    case "DELETE":
    case "TRACE":
      cn.setRequestMethod(verb);
      break;
    default:
      // set a dummy POST verb
      cn.setRequestMethod("POST");
      try {
        // Change protected field called "method" of public class HttpURLConnection
        setProtectedFieldValue(HttpURLConnection.class, "method", cn, verb);
      } catch (Exception ex) {
        throw new IOException(ex);
      }
      break;
  }
}

public static <T> void setProtectedFieldValue(Class<T> clazz, String fieldName, T object, Object newValue) throws Exception {
    Field field = clazz.getDeclaredField(fieldName);

    field.setAccessible(true);
    field.set(object, newValue);
 }
soñar con mandarina
fuente
Funciona para conexiones http, pero no para https. La clase sun.net.www.protocol.https.HttpsURLConnectionImpl utiliza un campo "delegado" que contiene la conexión URL real. Así que debe cambiar allí en su lugar.
Per Cederberg
0

Puede encontrar una solución detallada que puede funcionar incluso si no tiene acceso directo al HttpUrlConnection(como cuando trabaja con Jersey Client aquí: solicitud de PATCH usando Jersey Client

Daniel L.
fuente
Gracias por tu respuesta. Pero, ¿qué crees que es mejor, usar httpUrlConnection directo o Jersey Client?
Duan Bressan
0

Si su servidor usa ASP.NET Core, simplemente puede agregar el siguiente código para especificar el método HTTP usando el encabezado X-HTTP-Method-Override, como se describe en la respuesta aceptada .

app.Use((context, next) => {
    var headers = context.Request.Headers["X-HTTP-Method-Override"];
    if(headers.Count == 1) {
        context.Request.Method = headers.First();
    }
    return next();
});

Simplemente agregue este código Startup.Configureantes de su llamada a app.UseMvc().

Peppe LG
fuente
0

En emulador de la API 16 recibí una excepción: java.net.ProtocolException: Unknown method 'PATCH'; must be one of [OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE].

Si bien una respuesta aceptada funciona, quiero agregar un detalle. En las nuevas API PATCHfunciona bien, por lo que, junto con https://github.com/OneDrive/onedrive-sdk-android/issues/16 , debe escribir:

if (method.equals("PATCH") && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
    httpConnection.setRequestProperty("X-HTTP-Method-Override", "PATCH");
    httpConnection.setRequestMethod("POST");
} else {
    httpConnection.setRequestMethod(method);
}

Cambié JELLY_BEAN_MR2a KITKATdespués de probar en API 16, 19, 21.

CoolMind
fuente
0

Conseguí el mío con un cliente de Jersey. La solución alternativa fue:

Client client = ClientBuilder.newClient();
client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
Daniel Jipa
fuente
0

Hemos enfrentado el mismo problema con un comportamiento ligeramente diferente. Estábamos usando la biblioteca apache cxf para hacer el resto de llamadas. Para nosotros, PATCH funcionaba bien hasta que hablamos con nuestros servicios falsos que funcionaban a través de http. En el momento en que nos integramos con los sistemas reales (que estaban sobre https), comenzamos a enfrentar el mismo problema con el seguimiento de la pila siguiente.

java.net.ProtocolException: Invalid HTTP method: PATCH  at java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:428) ~[na:1.7.0_51]   at sun.net.www.protocol.https.HttpsURLConnectionImpl.setRequestMethod(HttpsURLConnectionImpl.java:374) ~[na:1.7.0_51]   at org.apache.cxf.transport.http.URLConnectionHTTPConduit.setupConnection(URLConnectionHTTPConduit.java:149) ~[cxf-rt-transports-http-3.1.14.jar:3.1.14]

El problema estaba sucediendo en esta línea de código

connection.setRequestMethod(httpRequestMethod); in URLConnectionHTTPConduit class of cxf library

Ahora la verdadera razón del fracaso es que

java.net.HttpURLConnection contains a methods variable which looks like below
/* valid HTTP methods */
    private static final String[] methods = {
        "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
    };

Y podemos ver que no hay un método PATCH definido, por lo que el error tiene sentido. Probamos muchas cosas diferentes y revisamos el desbordamiento de la pila. La única respuesta razonable fue utilizar la reflexión para modificar la variable de métodos para inyectar otro valor "PATCH". Pero de alguna manera no estábamos convencidos de usar eso, ya que la solución era una especie de pirateo y es demasiado trabajo y podría tener un impacto, ya que teníamos una biblioteca común para hacer todas las conexiones y realizar estas llamadas REST.

Pero luego nos dimos cuenta de que la biblioteca cxf en sí misma está manejando la excepción y hay un código escrito en el bloque catch para agregar el método que falta usando la reflexión.

try {
        connection.setRequestMethod(httpRequestMethod);
    } catch (java.net.ProtocolException ex) {
        Object o = message.getContextualProperty(HTTPURL_CONNECTION_METHOD_REFLECTION);
        boolean b = DEFAULT_USE_REFLECTION;
        if (o != null) {
            b = MessageUtils.isTrue(o);
        }
        if (b) {
            try {
                java.lang.reflect.Field f = ReflectionUtil.getDeclaredField(HttpURLConnection.class, "method");
                if (connection instanceof HttpsURLConnection) {
                    try {
                        java.lang.reflect.Field f2 = ReflectionUtil.getDeclaredField(connection.getClass(),
                                                                                     "delegate");
                        Object c = ReflectionUtil.setAccessible(f2).get(connection);
                        if (c instanceof HttpURLConnection) {
                            ReflectionUtil.setAccessible(f).set(c, httpRequestMethod);
                        }

                        f2 = ReflectionUtil.getDeclaredField(c.getClass(), "httpsURLConnection");
                        HttpsURLConnection c2 = (HttpsURLConnection)ReflectionUtil.setAccessible(f2)
                                .get(c);

                        ReflectionUtil.setAccessible(f).set(c2, httpRequestMethod);
                    } catch (Throwable t) {
                        //ignore
                        logStackTrace(t);
                    }
                }
                ReflectionUtil.setAccessible(f).set(connection, httpRequestMethod);
                message.put(HTTPURL_CONNECTION_METHOD_REFLECTION, true);
            } catch (Throwable t) {
                logStackTrace(t);
                throw ex;
            }
        }

Ahora, esto nos dio algunas esperanzas, así que dedicamos un tiempo a leer el código y descubrimos que si proporcionamos una propiedad para URLConnectionHTTPConduit.HTTPURL_CONNECTION_METHOD_REFLECTION, entonces podemos hacer que cxf ejecute el controlador de excepciones y nuestro trabajo se realiza ya que, de forma predeterminada, la variable será asignado a falso debido al código siguiente

DEFAULT_USE_REFLECTION = 
        Boolean.valueOf(SystemPropertyAction.getProperty(HTTPURL_CONNECTION_METHOD_REFLECTION, "false"));

Así que esto es lo que tuvimos que hacer para que esto funcione.

WebClient.getConfig(client).getRequestContext().put("use.httpurlconnection.method.reflection", true);

o

WebClient.getConfig(client).getRequestContext().put(HTTPURL_CONNECTION_METHOD_REFLECTION, true);

Donde WebClient es de la propia biblioteca cxf.

Espero que esta respuesta ayude a alguien.

Sanjay Bharwani
fuente
0
 **CloseableHttpClient http = HttpClientBuilder.create().build();
            HttpPatch updateRequest = new HttpPatch("URL");
            updateRequest.setEntity(new StringEntity("inputjsonString", ContentType.APPLICATION_JSON));
            updateRequest.setHeader("Bearer", "auth");
            HttpResponse response = http.execute(updateRequest);
JSONObject result = new JSONObject(IOUtils.toString(response.getEntity().getContent()));**

complemento de maven


> <dependency>
>                 <groupId>org.apache.httpcomponents</groupId>
>                 <artifactId>httpclient</artifactId>
>                 <version>4.3.4</version>
>                 <!-- Exclude Commons Logging in favor of SLF4j -->
>                 <exclusions>
>                     <exclusion>
>                         <groupId>commons-logging</groupId>
>                         <artifactId>commons-logging</artifactId>
>                     </exclusion>
>                 </exclusions> 
>             </dependency>

usa esto realmente te ayudaría

ks perumal
fuente
0

En java 11+ puede usar la clase HttpRequest para hacer lo que quiera:

import java.net.http.HttpRequest;

HttpRequest request = HttpRequest.newBuilder()
               .uri(URI.create(uri))
               .method("PATCH", HttpRequest.BodyPublishers.ofString(message))
               .header("Content-Type", "text/xml")
               .build();
simonalexander2005
fuente