Skip to main content

NG Remote Proxies

The next-generation engine adds a family of declarative capabilities to the REST proxy annotations (@Remote, @RemoteOp, and the verb annotations @RemoteGet / @RemotePost / @RemotePut / @RemoteDelete / @RemotePatch). This page covers those capabilities; the client itself is described in the Next-Generation REST Client overview.

Mostly next-gen engine only

Most features on this page are honored only by the next-generation proxy engine obtained via RestClient.remote(MyProxy.class) → RemoteClient. The classic engine obtained via RestClient.getRemote(...) ignores those annotation members; where an attribute is visible on a shared annotation, its Javadoc states that the classic engine currently ignores it. The one exception is dynamic URLs and base overrides (@Url / baseUrl()), which the classic engine also honors, with the same SSRF guardrail.

// Next-gen proxy — honors everything on this page.
MyProxy proxy = client.remote(MyProxy.class);

// Classic proxy — also honors @Url / baseUrl() (see below); ignores the other next-gen-only
// members on this page.
MyProxy classic = client.getRemote(MyProxy.class, "https://api.example.com");

Constant Part Values​

@Remote (interface) and @RemoteOp / the verb annotations (method) can declare always-applied constant headers, query parameters, and form-data fields that are emitted on every call — with no corresponding method parameter and no dummy null argument.

  • Interface-level: @Remote(headers=…, queryData=…, formData=…).
  • Method-level: @RemoteOp(headers=…, queryData=…, formData=…) (and the verb annotations).
  • headers use the "Name: value" form; queryData / formData use the "name=value" form.
  • All values resolve through VarResolver.DEFAULT (e.g. "$S{mySystemProperty}").
  • A method-level constant of the same name takes precedence over an interface-level constant.
  • A caller-supplied parameter value of the same name still composes with the constant (the constant is not suppressed). This is distinct from parameter-level def defaults, which only fill a null argument.
Constant path values are out of scope

There is no constant path attribute at either level. Path segments remain driven by the path template's {var} tokens and @Path parameters.

@Remote(
path="/api",
queryData="api_key=$S{petstore.apiKey}" // every call carries ?api_key=…
)
public interface PetStore {

@RemoteGet(
path="/pets",
headers="X-View: full", // every getPets() call sends this header
queryData="view=full" // …and ?view=full
)
Pet[] getPets();
}
@Remote(headerList=…) is classic-only

The @Remote(headerList=…) member (a supplier of an Apache-HttpClient HeaderList) remains classic-only. The next-gen engine does not consume it — use the transport-agnostic headers="Name: value" form instead.

Content Body Defaults​

The next-gen engine honors @Content(def=…) — both the parameter-level default (used when a body argument is null) and the param-less, method-level constant body (used when a method has no body parameter at all). This matches the classic engine's behavior and the documentation under Default Values. Provided values still win.

@Remote(path="/petstore")
public interface PetStore {

// Parameter-level default — fills a null argument.
@RemotePost("/pets")
Pet addPet(@Content(def="{name:'Unknown',price:0}") CreatePet pet);

// Param-less constant body — sent on every call.
@RemotePost("/pets/ping")
@Content(def="{}")
void ping();
}

Per-Part Serializers​

A specific request part (query / header / path / form-data) can be marshalled with a custom HttpPartSerializer via @HttpPartMarshalling(serializer=…), instead of the engine's default OpenApiSerializer. The annotation can sit on a parameter, on the method, or on the interface, with precedence parameter › method › interface; when absent, the existing default serializer is used unchanged.

@Remote(path="/api")
public interface MyApi {

@RemoteGet("/search")
String search(
@Query("filter")
@HttpPartMarshalling(serializer=MyFilterPartSerializer.class) // this part only
Filter filter
);
}
Serializer side only

Only the serializer (request) side is wired in the next-gen engine; the parser member is not consumed because the next-gen engine has no response-part parse path. This applies to HTTP parts only — request bodies are handled by per-method format selection and full serializers, not HttpPartSerializer.

Call Policy: Interceptors, Timeouts, Retries, throwOnError​

@Remote and @RemoteOp / the verb annotations can declare cross-cutting call behavior declaratively, instead of configuring it only on the RestClient builder. See also the Interceptors page.

MemberTypeBehavior
interceptors()Class<?>[]Classes implementing the client RestCallInterceptor SPI (public no-arg constructor).
timeout()StringPer-call response/read timeout as a duration string (e.g. "30s", "1500ms").
retries()intMaximum automatic retry attempts (0 = disabled).
retryNonIdempotent()booleanOpt POST/PATCH into auto-retry.
throwOnError()booleanThrow a generic exception on an unmatched error response.
@Remote(path="/api", interceptors=AuthInterceptor.class, timeout="30s")
public interface MyApi {

@RemoteGet(path="/flaky", retries=3, throwOnError=true)
Data getData();

@RemotePost(path="/orders", retries=2, retryNonIdempotent=true)
Order createOrder(@Content Order order);
}

Interceptors are applied as a union in the order builder → interface → method (the method-level interceptor runs closest to the call).

Timeout / retries / throwOnError scalars follow method › interface › builder default precedence (throwOnError and retryNonIdempotent are the logical-OR of the method and interface values). The per-call timeout is the response/read timeout; the connect timeout remains a client-level setting.

Timeouts and transports

The declarative timeout() is passed to the transport as the per-request timeout. Of the built-in transports, only JavaHttpTransport applies it; see the transport feature matrix.

Safe retries are deliberately conservative (hard gates, all must pass):

  • Trigger — a connection failure, or a retryable HTTP status (429 or any 5xx), with a short exponential backoff between attempts.
  • Idempotent verbs only — GET/PUT/DELETE/HEAD auto-retry; POST/PATCH retry only when retryNonIdempotent=true.
  • Repeatable bodies only — a request whose body is not repeatable (e.g. a streaming InputStream / Reader body) is never resent. A serialized-POJO body is repeatable.
  • Buffered return modes only — retries are disabled for RESPONSE, raw InputStream / Reader returns, streaming cursors, and Future / CompletableFuture returns.

throwOnError composes after the typed-exception mapping from the method's throws clause: a declared exception type whose status matches the response is still thrown in preference; only when no declared type matches and the status is >=400 does the generic exception fire.

Dynamic URLs and Base Overrides​

Honored by both engines

Unlike the other declarative features on this page, @Url and baseUrl() are honored by both the next-gen engine (RestClient.remote(...)) and the classic engine (RestClient.getRemote(...)). The classic engine resolves the same precedence order and enforces the same SSRF guardrail described below.

Two complementary ways to point a call at a runtime- or annotation-chosen endpoint:

  • @Url parameter (org.apache.juneau.http.Url) — the Retrofit @Url pattern. The argument's value becomes the effective URL for that single call. A value containing a scheme (://) is absolute — it replaces the whole endpoint and bypasses the client root URL; a value with no scheme is relative — it resolves against the client root URL only (not against the interface @Remote(path)). {var} tokens are still filled by @Path parameters. At most one @Url per method; a null/blank value is rejected.
  • baseUrl() attribute on @Remote / @RemoteOp / the verb annotations — substitutes only the authority+root of the URL while preserving the interface base path, the method path, and {var} templating. Resolves through VarResolver.DEFAULT.

Precedence (most-specific first): @Url parameter › method baseUrl › interface baseUrl → client rootUrl.

@Remote(path="/api", baseUrl="$S{myservice.baseUrl}")
public interface MyApi {

// Caller chooses the endpoint at runtime.
@RemoteGet
String fetch(@Url String url);

// Overrides the host for this one method only.
@RemoteGet(path="/health", baseUrl="http://health-host:8080")
String health();
}
SSRF guardrail

Only http / https schemes are permitted on a URL/base override — other schemes (file:, gopher:, jar:, …) are rejected with a clear error, allowPrivateUrls or not.

Beyond the scheme check, every absolute effective URL (an absolute @Url value, a baseUrl() override, or a default-path URL that already carries a scheme) is checked against a deny-private policy by default: loopback (127.0.0.1, ::1), RFC1918/unique-local private ranges, link-local (including the 169.254.169.254 / 169.254.170.2 cloud-metadata addresses), localhost, and *.metadata.google.internal are all rejected. This is enforced at connect time against the actually-resolved address (pin-on-connect) — not just lexically against the hostname string — so a DNS name that resolves to a blocked address is rejected even if the hostname itself looks public, and a rebinding attack (blocked address swapped in after the initial check) cannot bypass it. Every hop of a redirect chain on a guarded call is re-validated the same way before it is followed; scheme-less relative @Url values (resolved against the client root) are not re-checked.

Opt out for an intentional local-dev or intranet target via allowPrivateUrls(true) on the client builder, @Remote(allowPrivateUrls=true) on the interface, or the RestClient.allowPrivateUrls system property — any one of the three is sufficient (there is no per-method override), and http/ https is still required either way.

Declarative Multipart​

Beyond manually building a multipart body, the next-gen engine supports a Retrofit-style declarative multipart/form-data surface:

  • @Multipart (org.apache.juneau.http.remote.Multipart) — a method-level marker. The multipart/form-data Content-Type (with a generated boundary) is applied automatically.
  • @Part (org.apache.juneau.http.Part) — a parameter annotation with name() / value() (synonym), fileName(), and contentType() members.
@Remote(path="/api")
public interface UploadService {

@RemotePost("/upload")
@Multipart
String upload(
@Part("title") String title,
@Part(name="attachment", fileName="report.pdf", contentType="application/pdf") byte[] data
);
}

Accepted part-source types (mapped automatically from the argument type):

Argument typePart behavior
String / CharSequence / scalarsText field
byte[]Binary, sent verbatim (repeatable)
FileStreamed; fileName defaults to the file's name
InputStreamStreamed (one-shot; makes the body non-repeatable)
ReaderStreamed as UTF-8 (one-shot)
HttpBodyUsed directly
any other object (bean)Serialized with the client's default serializer

A null @Part argument contributes no part. File / stream / reader / bean parts stream (they reuse the streaming bodies described under Streaming Bodies).

Body-mode exclusivity is validated at proxy-build time (IllegalArgumentException): a method is either multipart (@Multipart + one or more @Part) or single-body (@Content), never both; @Multipart with no @Part, or @Part without @Multipart, is also rejected.

Escape hatch (Option B)

For advanced cases you can still hand-assemble a MultipartBody and pass it as a single @Content body without @Multipart — that path is unchanged.

Per-Method Format Selection​

@Remote (interface default) and @RemoteOp / the verb annotations (method) expose contentType() and accept() media-type attributes that drive marshaller selection — not merely header values. This lets a single method switch its actual wire format without changing the client default for every other call (e.g. one legacy XML endpoint on an otherwise-JSON API, or a binary format on a few endpoints). See also Content-Type Negotiation.

  • contentType selects the matching registered request serializer from the client's serializer set (so the body bytes are actually written in that format) and sets a single, clean Content-Type header — replacing the serializer's default content type with no duplicate header.
  • accept sets the Accept header and acts as a fallback parser only. The response Content-Type stays authoritative for parser selection; the accept media type is used only when the response is unlabeled or its Content-Type matches no registered parser.
  • No-match media type falls back to the default marshaller but still sends the overridden label (supporting vendor types such as application/vnd.example.v2+json).

Precedence: method › interface › client default. A dedicated contentType/accept attribute beats a constant Content-Type/Accept header (the constant is dropped, no duplicate), but a genuinely caller-supplied @Header("Content-Type"/"Accept") parameter still wins.

@Remote(path="/api")            // interface defaults to the client's JSON marshaller
public interface MyApi {

@RemoteGet("/items")
Item[] getItems(); // JSON request/response

// This one legacy endpoint speaks XML in both directions.
@RemotePost(path="/legacy", contentType="application/xml", accept="application/xml")
Result postLegacy(@Content Payload payload);
}

Streaming Request and Response Bodies​

The next-gen engine streams large bodies in both directions instead of buffering them in memory, matching the classic engine's efficiency. See also Streaming Cursors.

  • Streaming POJO request bodies — a POJO @Content body is serialized straight to the wire (chunked, Content-Length: -1) rather than pre-serialized to a String/byte[]. This body is repeatable, so it is safe to auto-retry.
  • Streaming Reader request bodies — a Reader @Content body streams as UTF-8 rather than being drained to a String first. (Non-repeatable — never auto-retried.)
  • Reader and InputStream return types — a method may declare a Reader or InputStream return type and receive a lazy stream over the live response. The caller owns the stream; the underlying connection is released when the caller closes it. Always close the returned stream (try-with-resources).
@Remote(path="/api")
public interface MyApi {

// Streams the POJO body straight to the socket.
@RemotePost("/bulk")
void bulkUpload(@Content List<Record> records);

// Caller owns the returned stream — close it to release the connection.
@RemoteGet("/export")
InputStream export();
}
MyApi api = client.remote(MyApi.class);
try (InputStream in = api.export()) {
in.transferTo(System.out);
}

See Also​