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.
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). headersuse the"Name: value"form;queryData/formDatause 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
defdefaults, which only fill anullargument.
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-onlyThe @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
);
}
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.
| Member | Type | Behavior |
|---|---|---|
interceptors() | Class<?>[] | Classes implementing the client RestCallInterceptor SPI (public no-arg constructor). |
timeout() | String | Per-call response/read timeout as a duration string (e.g. "30s", "1500ms"). |
retries() | int | Maximum automatic retry attempts (0 = disabled). |
retryNonIdempotent() | boolean | Opt POST/PATCH into auto-retry. |
throwOnError() | boolean | Throw 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.
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 (
429or any5xx), with a short exponential backoff between attempts. - Idempotent verbs only —
GET/PUT/DELETE/HEADauto-retry;POST/PATCHretry only whenretryNonIdempotent=true. - Repeatable bodies only — a request whose body is not repeatable (e.g. a streaming
InputStream/Readerbody) is never resent. A serialized-POJO body is repeatable. - Buffered return modes only — retries are disabled for
RESPONSE, rawInputStream/Readerreturns, streaming cursors, andFuture/CompletableFuturereturns.
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
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:
@Urlparameter (org.apache.juneau.http.Url) — the Retrofit@Urlpattern. 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@Pathparameters. At most one@Urlper method; anull/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 throughVarResolver.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();
}
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. Themultipart/form-dataContent-Type(with a generated boundary) is applied automatically.@Part(org.apache.juneau.http.Part) — a parameter annotation withname()/value()(synonym),fileName(), andcontentType()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 type | Part behavior |
|---|---|
String / CharSequence / scalars | Text field |
byte[] | Binary, sent verbatim (repeatable) |
File | Streamed; fileName defaults to the file's name |
InputStream | Streamed (one-shot; makes the body non-repeatable) |
Reader | Streamed as UTF-8 (one-shot) |
HttpBody | Used 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.
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.
contentTypeselects 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, cleanContent-Typeheader — replacing the serializer's default content type with no duplicate header.acceptsets theAcceptheader and acts as a fallback parser only. The responseContent-Typestays authoritative for parser selection; theacceptmedia type is used only when the response is unlabeled or itsContent-Typematches 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
@Contentbody is serialized straight to the wire (chunked,Content-Length: -1) rather than pre-serialized to aString/byte[]. This body is repeatable, so it is safe to auto-retry. - Streaming
Readerrequest bodies — aReader@Contentbody streams as UTF-8 rather than being drained to aStringfirst. (Non-repeatable — never auto-retried.) ReaderandInputStreamreturn types — a method may declare aReaderorInputStreamreturn 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
- Next-Generation REST Client — client overview
- REST Proxies — the classic proxy annotations and default values
- NG Interceptors —
RestCallInterceptorhooks - NG Client Security — SSRF guard, DNS pinning, redirect handling
- HTTP Transport Adapters — transport feature matrix