private <T> T asyncRequest(HttpRequest request, BodyHandler<T> bodyHandler, TaskMonitor monitor)
			throws CancelledException, IOException {
		CompletableFuture<HttpResponse<T>> futureResponse =
			HttpClients.getHttpClient().sendAsync(request, bodyHandler);
		CancelledListener l = () -> futureResponse.cancel(true);
		monitor.addCancelledListener(l);

		try {
			HttpResponse<T> response = futureResponse.get();

			int statusCode = response.statusCode();
			if (statusCode != HttpURLConnection.HTTP_OK) {
				Msg.debug(this, "HTTP request [%s], response: %d, body: %s".formatted(request.uri(),
					statusCode, resultToSafeStr(response.body().toString())));
				throw new IOException(
					"Bad HTTP result [%d] for request [%s]".formatted(statusCode, request.uri()));
			}

			String responseContentType =
				response.headers().firstValue(CONTENT_TYPE_HEADER).orElse("missing");
			if (!CONTENT_TYPE_JSON.equals(responseContentType)) {
				throw new IOException(
					"Bad content-type in result: [%s]".formatted(responseContentType));
			}

			return response.body();
		}
		catch (InterruptedException e) {
			throw new CancelledException("Request canceled");
		}
		catch (ExecutionException e) {
			// if possible, unwrap the exception that happened inside the future
			Throwable cause = e.getCause();
			Msg.error(this, "Error during HTTP request [%s]".formatted(request.uri()), cause);
			throw (cause instanceof IOException)
					? (IOException) cause
					: new IOException("Error during HTTP request", cause);
		}
		finally {
			monitor.removeCancelledListener(l);
		}

	}
Returns a list of languages that the LibreTranslate server supports.

@param monitor {@link TaskMonitor}
@return list of {@link SupportedLanguage} records
@throws IOException if error connecting or excessive time to respond
@throws CancelledException if cancelled
Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/string/translate/libretranslate/LibreTranslateStringTranslationService.java