7 HttpURLConnection¶
HttpURLConnection¶
La clase HttpUrlConnection
extiende de URLConnection
y proporciona a la conexión las funcionalidades específicas del protocolo HTTP.
Cada instancia de la clase HttpURLConnection
debe ser usada para un única conexión, es decir, que si posteriormente deseamos realizar otra conexión deberemos crear un nuevo objeto HttpURLConnection
y no reusar el anterior. Aún así, con objeto de optimizar recursos, el sistema hace que la conexión de red interna sea compartida de forma transparente para el programador con otras instancias de HttpURLConnection
.
Si queremos obtener un objeto que represente una conexión con un recurso web apuntado por una determinada URL, deberemos llamar al método openConnection()
del objeto URL
, que retorna un objeto URLConnection
.
Dado que nuestra conexión usa el protocolo HTTP, realizaremos un cast explícito a HttpURLConnection
del objeto URLConnection
retornado por el método openConnection()
.
El método openConnection()
puede lanzar la excepción IOException
si se produce un error de entrada o salida mientras se está abriendo la conexión.
Debemos tener en cuenta que el método anterior simplemente crea un objeto que representa la conexión, pero no realiza la conexión en sí. Para que se lleve a cabo la conexión deberemos llamar al método connect()
, como vimos anteriormente.
Una vez que tengamos el objeto HttpURLConnection
podemos configurar la conexión a través de una serie de métodos. Todos estos métodos lanzan la excepción ProtocolException
si ya hubiéramos establecido la conexión. Por este motivo siempre debemos establecer estos métodos antes de llamar al método connect()
:
setRequestMethod(methodString)
: Establece el método del protocolo HTTP que se quiere usar. Puede recibir los valoresGET
(valor por defecto),POST
,PUT
,DELETE
, y otro menos usados. Este método puede lanzar la excepciónProtocolException
si el método indicado no es válido.setConnectionTimeout(timeoutMilis)
: Establece el tiempo máximo que se esperará para establecer la conexión cuando esta se inicie, transcurrido el cual se lanzará una excepción.setReadTimeout(timeoutMilis)
: Establece el tiempo máximo que se esperará para realizar una operación de lectura de la conexión, transcurrido el cual se lanzará una excepción.setRequestProperty(headerName, headerValue)
: Establece un determinado encabezado en la petición HTTP.setDoInput(boolean)
: Una conexión HTTP a una URL puede ser usada tanto para obtener información como para enviar información. Mediante el métododoInput(true)
indicamos que querremos leer la información recibida desde el servidor. Normalmente no llamaremos a este método porque es el comportamiento por defecto.setDoOutput(boolean)
: Si queremos enviar datos al servidor será necesario que lo indiquemos explícitamente llamando al métododoOutput(true)
, ya que por defecto esta posibilidad está deshabitada.
// Por defecto el método será GET y estará activada la posibilidad de leer la
// respuesta, por lo que en este caso no será necesario llamar a
// setRequestMethod("GET") ni a setDoInput(true).
httpUrlConnection.add RequestProperty("User-Agent",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)");
httpUrlConnection.setConnectTimeout(5000);
httpUrlConnection.setReadTimeout(5000);
Una vez configurada la petición, iniciaremos la conexión llamando al método connect()
, que la establecerá siempre y cuando no hubiera sido establecida anteriormente. Este método lanzará la excepción SocketTimeoutException
si transcurre el timeout indicado mediante setConnectionTimeout()
sin que haya sido posible establecer la conexión. También podrá lanzar la excepción IOException
si se produce un error de entrada/salida durante la conexión. Se trata de una llamada síncrona, que finalizará de ejecutarse cuando se obtenga la respuesta por parte del servidor.
Debemos tener en cuenta que los métodos del objeto HttpUrlConnection
cuyo resultado dependa de que se hubiera establecido la conexión, realizarán la conexión implícitamente sin que sea necesario llamar explícitamente al método connect()
. Es el caso de los métodos getResponseCode()
, getResponseMessage()
, getInputStream()
, getOutputStream()
, getContentLength()
, etc.
Una vez obtenida la respuesta del servidor podemos proceder a leer la información enviada por el mismo. De la respuesta obtenida el elemento más importante es el código de respuesta, también llamado HTTP status code, que corresponde a un valor entero estándar indicativo que cómo ha ido la petición. Para obtener dicho valor podemos llamar al método getResponseCode()
. Dicho método lanzará la excepción IOException
si se produjo un error en la conexión con el servidor. La clase HttpURLConnection
define una serie de constantes con los códigos de respuesta más significativos. El más deseado de ellos es HttpURLConnection.HTTP_OK
, cuyo valor es 200
, y que es indicativo de que todo ha ido bien (los códigos de respuesta en el rango 200-299
son positivos). Por tanto, una vez realizada la conexión realizaremos la comprobación del código de respuesta de la siguiente manera:
// Es llamado implícitamente por el método getResponseCode().
// httpUrlConnection.connect();
if (httpUrlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// Todo ha ido bien, podemos proceder a leer la respuesta
// ...
}
Si queremos obtener la descripción textual del código de respuesta enviada por el servidor podemos usar el método getResponseMessage()
.
Para que podamos leer el cuerpo de la respuesta, la clase HttpURLConnection
posee el método getInputStream()
, que retorna un objeto InputStream
, que corresponde a un flujo de datos de entrada que podremos proceder a leer de forma similar a si proviniera, por ejemplo, de un fichero. Este método puede lanzar la excepción IOException
si se produce un error de entrada/salida durante la creación del flujo, o si el código de respuesta corresponde a un valor indicativo de que se ha producido un error al procesar la petición. En este último caso se puede usar el método getErrorStream()
para obtener un flujo con la información de error.
Debemos tener en cuenta que el flujo retornado por el método anterior no usa ningún tipo de buffer, por lo que normalmente al usarlo lo envolveremos en un BufferedInputStream
creado a partir de él que sí actúe como buffer. Otra posibilidad es usar un BufferedReader
para leer del flujo original.
A continuación se muestra un método de utilidad que recibe un flujo de entrada y retorna una cadena de caracteres con todo el contenido leído desde dicho flujo:
public static String readInputStream(InputStream inputStream) throws IOException {
// Utiliza la estructura try-with-resources de Java 8 para ue el bufferedReader
// se cierre automáticamente, lo que producirá que se cierre el inputStream.
try (BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(inputStream))) {
// Usa los streams para leer todas las líneas.
return bufferedReader.lines().collect(Collectors.joining("\n"));
}
}
En Java 9 es aún más sencillo, ya que podemos usar directamente:
Una vez ha sido leído el cuerpo de la respuesta, debemos proceder a realizar la desconexión, para lo que haremos uso del método disconnect()
del objeto HttpURLConnection
. Debemos tener en cuenta que dicha operación debe llevarse a cabo incluso si se ha producido algún error después de realizar la conexión, por lo que suele incluirse dentro de la rama finally
de un try-catch-finally
. Al realizar la desconexión se liberan los recursos de la misma para que puedan ser reusados. Por ejemplo:
// ...
try {
content = HttpUtils.readContent(httpUrlConnection.getInputStream());
} finally {
if (httpUrlConnection != null) {
httpUrlConnection.disconnect();
}
}
Petición GET¶
A modo de resumen de lo anterior, a continuación vemos el código de un método que retorna en forma de cadena de caracteres el contenido de la respuesta correspondiente a una petición GET a una URL:
private String doGetRequest(URL url) throws IOException {
String content = "";
HttpURLConnection httpUrlConnection;
try {
httpUrlConnection = (HttpURLConnection) url.openConnection()
httpUrlConnection.setConnectTimeout(5000);
httpUrlConnection.setReadTimeout(5000);
if (httpUrlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
content = HttpUtils.readContent(httpUrlConnection.getInputStream());
}
return content;
} finally {
if (httpUrlConnection != null) {
httpUrlConnection.disconnect();
}
}
}
Petición POST¶
Si nuestra petición va a enviar datos al servidor, deberemos indicárselo explícitamente al objeto HttpUrlConnection
mediante el método setDoOutput(true)
, que debe ser llamado antes de que se establezca la conexión.
Además, deberemos llamar al método setFixedLengthStreamingMode(int)
cuando sepamos de antemano el tamaño del cuerpo de la petición (tamaño de los datos que se quieren enviar), o al método setChunkedStreamingMode(int)
si no conocemos dicho tamaño, que recibe el tamaño de los partes en el que se quieren enviar los datos, o 0
si queremos que se use un tamaño por defecto. Si no llamamos a ninguno de ambos métodos, el objeto HttpURLConnection
se verá forzado a almacenar en memoria el cuerpo de petición completo antes de poder transmitirlo.
También deberemos obtener el flujo de salida de la conexión, para que podamos escribir en él los datos. Para ello llamaremos al método getOutputStream()
del objeto HttpUrlConnection
, que realizará la conexión implícitamente si no se había establecido aún. Este método puede lanzar la excepción IOException
si se produce un error de entrada/salida durante la creación del flujo.
El objeto OutputStream
retornado por el método anterior no usa ningún buffer, por lo que se recomienda envolverlo en algún BufferedStream
o simplemente usar para escribir en él algún BufferedWriter
.
Debemos tener en cuenta que la cabecera Content-Type
es la que indica el formato en el que se van a enviar los datos al servidor. Evidentemente los datos se deben enviar en el formato el que el servidor espere recibirlos. Uno de los formatos más habituales es application/x-www-form-urlencoded
, que indica que los datos van a enviarse en el cuerpo de la petición en forma de variable1=valor1&variable2=valor2
, de forma similar a como si fueran incluido en la propia URL. Si se van a enviar los datos en formato JSON usaremos el valor application/json
para dicha cabecera.
A modo de resumen, en el siguiente código vemos un método que envía datos a una URL mediante el método POST del protocolo HTTP y obtiene la respuesta en forma de cadena de texto:
private String doPostUrlEncodedRequest(URL url, Map<String, String> parameters)
throws IOException {
String content = "";
try {
HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection.setRequestMethod("POST");
httpUrlConnection.setConnectTimeout(5000);
httpUrlConnection.setReadTimeout(5000);
httpUrlConnection.setDoOutput(true);
if (parameters != null) {
try (PrintWriter writer = new PrintWriter (
httpUrlConnection.getOutputStream())) {
boolean addAmpersand = false;
for (Map.Entry<String, String> p : parameters.entrySet()) {
if (addAmpersand) {
writer.write("&");
}
else {
addAmpersand = true;
}
writer.write(p.getKey() + "=" + p.getValue());
}
writer.flush();
}
}
if (httpUrlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
content = readContent(httpUrlConnection.getInputStream());
}
return content;
} finally {
if (httpUrlConnection != null) {
httpUrlConnection.disconnect();
}
}
}
Descarga de una imagen¶
En el siguiente código vemos un ejemplo de cómo obtener mediante el protocolo HTTP una imagen alojada en la web:
Descarga de una imagen con HttpURLConnection
public Bitmap downloadImage(URL url) throws IOException {
Bitmap bitmap = null;
try {
HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
byte[] result;
try(InputStream inputStream = conexion.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] b = new byte[1024];
while (inputStream.read(b) != -1) {
outputStream.write(b);
}
result = outputStream.toByteArray();
}
bitmap = BitmapFactory.decodeByteArray(result, 0, result.length);
return bitmap;
} finally {
if (httpUrlConnection != null) {
httpUrlConnection.disconnect();
}
}
}
Proyecto HttpURLConnection¶
En este proyecto vamos a realizar una aplicación que realiza distintos tipos de peticiones HTTP a un servicio REST de ejemplo, a través de la clase HttpURLConnection
.
Cada petición y su correspondiente respuesta son mostradas por consola. Para ello creamos una función general que reciba como argumentos los datos necesarios para configurar la petición (estamos usando Java 9).
import java.util.Map;
import java.util.Scanner;
public class Main {
public static final int TIMEOUT_MILLIS = 5000;
private final Scanner scanner = new Scanner(System.in);
Main() {
int selectedOption = showMenu();
processOption(selectedOption);
}
public static void main(String[] args) {
new Main();
}
private int showMenu() {
System.out.println("\nMENU");
System.out.println("1. Show all posts");
System.out.println("2. Show post");
System.out.println("3. Show user's posts");
System.out.println("4. Create post");
System.out.println("5. Update post");
System.out.println("6. Update post partially");
System.out.println("7. Delete post");
System.out.println("8. Show only headers");
System.out.println("9. Show access options");
System.out.print("Select an option: ");
try {
return scanner.nextInt();
} catch (Exception e) {
scanner.nextLine();
return 0;
}
}
private void processOption(int selectedOption) {
switch (selectedOption) {
case 1:
showPosts();
break;
case 2:
showPost(1);
break;
case 3:
showUserPosts(1);
break;
case 4:
createPost("{\"title\": \"Baldomero\", \"body\": \"Llegate Ligero\", \"userId\": 1}");
break;
case 5:
updatePost(1, "{\"id\": 1, \"title\": \"Baldomero\", \"body\": \"Llegate Ligero\", \"userId\": 1}");
break;
case 6:
patchPost(1, "{\"title\": \"Baldomero\"}");
break;
case 7:
deletePost(1);
break;
case 8:
showPostsHeaders();
break;
case 9:
showPostsAccessOptions();
break;
}
}
private void showPosts() {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts", "GET",
null, null, TIMEOUT_MILLIS).join();
}
private void showPost(int postId) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts/" + postId, "GET",
null, null, TIMEOUT_MILLIS).join();
}
private void createPost(String jsonPost) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts", "POST",
Map.of("Content-type", "application/json; charset=UTF-8"),
jsonPost.getBytes(), TIMEOUT_MILLIS).join();
}
private void updatePost(int postId, String jsonPost) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts/" + postId, "PUT",
Map.of("Content-type", "application/json; charset=UTF-8"),
jsonPost.getBytes(), TIMEOUT_MILLIS).join();
}
private void patchPost(int postId, String jsonPost) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts/" + postId, "PATCH",
Map.of("Content-type", "application/json; charset=UTF-8"),
jsonPost.getBytes(), TIMEOUT_MILLIS).join();
}
private void deletePost(int postId) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts/" + postId, "DELETE",
null, null, TIMEOUT_MILLIS).join();
}
private void showUserPosts(int userId) {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/users/" + userId + "/posts", "GET",
null, null, TIMEOUT_MILLIS).join();
}
private void showPostsHeaders() {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts", "HEAD",
null, null, TIMEOUT_MILLIS)
.exceptionally(this::showError)
.join();
}
private void showPostsAccessOptions() {
HttpUtils.doHttpRequestAsync("https://jsonplaceholder.typicode.com/posts", "OPTIONS",
null, null, TIMEOUT_MILLIS).join();
}
private Void showError(Throwable e) {
System.out.println(e.toString());
return null;
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
public class HttpUtils {
private HttpUtils() {
}
public static CompletableFuture<Void> doHttpRequestAsync(String urlString, String requestMethod, Map<String, String> requestHeaders,
byte[] requestBody, int timeout) {
return CompletableFuture.runAsync(
() -> doHttpRequest(urlString, requestMethod, requestHeaders, requestBody, timeout));
}
public static CompletableFuture<Void> doHttpRequestAsync(String urlString, String requestMethod, Map<String, String> requestHeaders,
byte[] requestBody, int timeout, Executor executor) {
return CompletableFuture.runAsync(
() -> doHttpRequest(urlString, requestMethod, requestHeaders, requestBody, timeout),
executor);
}
private static void doHttpRequest(String urlString, String requestMethod, Map<String, String> requestHeaders,
byte[] requestBody, int timeout) {
HttpURLConnection httpUrlConnection = null;
try {
URL url = new URL(urlString);
// 1. Create connection with the URL.
httpUrlConnection = (HttpURLConnection) url.openConnection();
// 2. Setup request
// 2.1. Request method.
// PATCH method is not allowed as requesMethod, so is hacked as POST method.
if ("PATCH".equals(requestMethod)) {
httpUrlConnection.setRequestProperty("X-HTTP-Method-Override", "PATCH");
httpUrlConnection.setRequestMethod("POST");
} else {
httpUrlConnection.setRequestMethod(requestMethod);
}
// 2.2. Request timeouts.
httpUrlConnection.setConnectTimeout(timeout);
httpUrlConnection.setReadTimeout(timeout);
// 2.3. Request headers
if (requestHeaders != null) {
for (Map.Entry<String, String> header : requestHeaders.entrySet()) {
httpUrlConnection.addRequestProperty(header.getKey(), header.getValue());
}
}
// 2.4. Request body.
if (requestBody != null) {
httpUrlConnection.setDoOutput(true);
try (OutputStream out = httpUrlConnection.getOutputStream()) {
out.write(requestBody);
out.flush();
}
}
// 3. Connect (optional, internally called by getResponseCode())
httpUrlConnection.connect();
// 4. Process response
// 4.1. Response code
System.out.print(httpUrlConnection.getResponseCode());
// 4.2. Response message
System.out.println(" " + httpUrlConnection.getResponseMessage());
// 4.2. Response headers.
if (httpUrlConnection.getHeaderFields() != null) {
for (Map.Entry<String, List<String>> responseHeader : httpUrlConnection.getHeaderFields().entrySet()) {
if (responseHeader.getKey() != null) {
System.out.print(responseHeader.getKey() + ": ");
}
System.out.println(String.join(", ", responseHeader.getValue()));
}
}
if (httpUrlConnection.getResponseCode() >= 200 && httpUrlConnection.getResponseCode() < 300) {
// 4.3 Response body
System.out.println("\n" + readInputStream(httpUrlConnection.getInputStream()));
}
} catch (IOException e) {
// 5. Show error.
System.out.println(e.getMessage());
} finally {
// 6. Disconnect
if (httpUrlConnection != null) {
httpUrlConnection.disconnect();
}
}
}
private static String readInputStream(InputStream inputStream) throws IOException {
return new String(inputStream.readAllBytes());
}
}
Problemas de HttpURLConnection¶
Hasta la versión 11 de Java, la única forma de trabajar con el protocolo HTTP era a través de la clase HttpURLConnection
, disponible desde Java 1.1. Sin embargo, esta clase tiene una serie de problemas:
- La API de
URLConnection
, de la que heredaHttpURLConnection
, fue diseñada para múltiples protocolos que ya no están en funcionamiento. - No es compatible con el protocolo HTTP/2, su versión más reciente.
- Sólo trabaja en modo síncrono, de manera que el hilo llamador queda bloqueado hasta que se recibe la respuesta.
- No es precisamente sencilla de usar ni mantener, y no es modular (no separa en clases los conceptos de cliente, petición y respuesta).
De hecho, si vemos el Proyecto HttpURLConnection comprobamos que aunque las peticiones se están realizado en un hilo secundario, no está configurando un cliente HTTP que después podamos reusar múltiples veces, sino que en cada petición debe configurarse. Por otra parte, la petición no se pasa en forma de objeto, sino que pasamos el verbo HTTP a usar, el cuerpo y las cabeceras de la petición de forma individual. Finalmente, el método construido no retorna una respuesta, sino que directamente la procesa, mostrándola por pantalla. No existe un objeto que represente la respuesta, con su código de respuesta y su cuerpo, de manera que podamos procesarla posteriormente.
Por todo lo anterior, muchos proyectos optaron por usar librerías externas para crear clientes HTTP, que solucionan los problemas que acabamos de HttpURLConnection que acabamos de describir. Las más famosas de estas librerías son Apache HTTP Client y OkHttp, que fueron creadas mucho antes de la aparición Java 8, y se convirtieron en el estándar de facto.
Sin embargo, Java 11 introdujo una nueva API para la creación de clientes HTTP compatibles con HTTP/2. Además, y a diferencia de la clase HttpURLConnection
, el nuevo cliente HTTP nos proporciona mecanismos para realizar y gestionar peticiones asíncronas, separando claramente los objetos que representan el cliente HTTP, la petición HTTP y la respuesta HTTP.
En siguientes apartados estudiaremos la librería OkHttp, que es ampliamente usada en programación para dispositivos Android, y el cliente HTTP introducido en la versión 11 de Java.