Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
955e07a
fix: compute URL expressions in the target after request flow in the …
predic8 Feb 15, 2026
e11eae9
refactor(core): use `getAuthority` instead of `getHost` for improved …
predic8 Feb 15, 2026
6eb7add
feat(core): add URL encoding support for template evaluation in targe…
predic8 Feb 15, 2026
583504a
docs(roadmap): update URL encoding details for template evaluation in…
predic8 Feb 15, 2026
fd737e8
feat(core): optimize expression handling for dynamic URLs with caching
predic8 Feb 15, 2026
58959bf
refactor(core): remove template expression caching from `Target` and …
predic8 Feb 15, 2026
428cb83
feat(core): introduce `Escaping` enum for customizable URL encoding i…
predic8 Feb 16, 2026
684e6c2
refactor(core): enhance URI handling and extend test coverage
predic8 Feb 16, 2026
4e66506
Merge branch 'master' into compute-url-expression-at-http-client-inte…
predic8 Feb 16, 2026
76ffa6d
feat(core): improve handling of illegal characters in URLs and enhanc…
predic8 Feb 17, 2026
620871d
refactor(core): simplify `URI` logic and enhance test readability
predic8 Feb 17, 2026
1f28df1
Merge branch 'master' into compute-url-expression-at-http-client-inte…
predic8 Feb 17, 2026
f52cf95
feat(core): enhance URI handling and replace `pathSeg` with `pathEncode`
predic8 Feb 18, 2026
2b39f1a
refactor(core): improve code readability and consistency in URI tests…
predic8 Feb 18, 2026
20a2f91
feat(core): introduce `URIFactory` for enhanced URI creation and add …
predic8 Feb 18, 2026
78fbd0e
test(core): extend URI test cases for edge scenarios in IPv4 and IPv6…
predic8 Feb 18, 2026
cefb219
feat(core): enhance template marker detection and URL evaluation in `…
predic8 Feb 19, 2026
3e3b860
Merge branch 'master' into compute-url-expression-at-http-client-inte…
predic8 Feb 19, 2026
479e90e
Merge branch 'master' into compute-url-expression-at-http-client-inte…
predic8 Feb 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
/* Copyright 2026 predic8 GmbH, www.predic8.com

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */

package com.predic8.membrane.annot.generator;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down
20 changes: 20 additions & 0 deletions core/src/main/java/com/predic8/membrane/core/http/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,26 @@ public void discardBody() {
}
}

/**
* Empties the body of the message.
* This method ensures the message contents accurately reflect a state where there is no body,
* and updates the headers accordingly to maintain consistency.
*/
public void emptyBody() {
try {
if (isBodyEmpty())
return;
} catch (IOException e) {
log.debug("", e);
}
discardBody(); // Read body before we replace it. Maybe there is one but it is not read
body = new EmptyBody();
header.removeFields(CONTENT_LENGTH);
header.removeFields(CONTENT_TYPE);
header.removeFields(CONTENT_ENCODING);
header.removeFields(TRANSFER_ENCODING);
}

public AbstractBody getBody() {
return body;
}
Expand Down
16 changes: 15 additions & 1 deletion core/src/main/java/com/predic8/membrane/core/http/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.transport.http.*;
import com.predic8.membrane.core.util.*;
import org.slf4j.*;

import java.io.*;
import java.net.*;
Expand All @@ -31,6 +32,8 @@

public class Request extends Message {

private static final Logger log = LoggerFactory.getLogger(Request.class);

private static final Pattern pattern = Pattern.compile("(.+?) (.+?) HTTP/(.+?)$");
private static final Pattern stompPattern = Pattern.compile("^(.+?)$");

Expand Down Expand Up @@ -166,6 +169,17 @@ public <T extends Message> T createSnapshot(Runnable bodyUpdatedCallback, BodyCo
return (T) result;
}

public void changeMethod(String newMethod) {
if (method.equalsIgnoreCase(newMethod))
return;

log.debug("Changing method from {} to {}", this.method, newMethod);
this.method = newMethod;

if (methodsWithoutBody.contains(newMethod.toUpperCase()))
emptyBody();
}
Comment thread
predic8 marked this conversation as resolved.

public final void writeSTOMP(OutputStream out, boolean retainBody) throws IOException {
out.write(getMethod().getBytes(UTF_8));
out.write(10);
Expand Down Expand Up @@ -257,7 +271,7 @@ public Builder header(String headerName, String headerValue) {
}

public Builder contentType(String value) {
req.getHeader().setContentType( value);
req.getHeader().setContentType(value);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@

import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
import static com.predic8.membrane.core.exchange.Exchange.*;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.Set.*;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static com.predic8.membrane.core.resolver.ResolverMap.combine;
import static com.predic8.membrane.core.util.URIFactory.*;

/**
* @description This interceptor adds the destination specified in the target
Expand All @@ -44,7 +44,7 @@
@MCElement(name = "dispatching", excludeFromFlow = true)
public class DispatchingInterceptor extends AbstractInterceptor {

private static final Logger log = LoggerFactory.getLogger(DispatchingInterceptor.class.getName());
private static final Logger log = LoggerFactory.getLogger(DispatchingInterceptor.class);

public DispatchingInterceptor() {
name = "dispatching interceptor";
Expand All @@ -56,11 +56,14 @@ public Outcome handleRequest(Exchange exc) {
exc.getDestinations().clear();
try {
exc.getDestinations().add(getForwardingDestination(exc));
} catch (IllegalArgumentException e) {
createInvalidCharacterProblemDetails(exc)
.detail(e.getMessage())
.buildAndSetResponse(exc);
return ABORT;
} catch (URISyntaxException e) {
var pd = user(getRouter().getConfiguration().isProduction(), "invalid-path")
.title("Request path contains an invalid character.")
var pd = createInvalidCharacterProblemDetails(exc)
.detail(getMessageForURISyntaxException(exc, e))
.internal("path", exc.getRequest().getUri())
.internal("destination", e.getInput());
if (e.getIndex() >= 0)
pd.internal("index", e.getIndex());
Expand All @@ -80,6 +83,12 @@ public Outcome handleRequest(Exchange exc) {
return CONTINUE;
}

private ProblemDetails createInvalidCharacterProblemDetails(Exchange exc) {
return user(getRouter().getConfiguration().isProduction(), "invalid-path")
.title("Request path contains an invalid character.")
.internal("path", exc.getRequest().getUri());
}

private static @NotNull String getMessageForURISyntaxException(Exchange exc, URISyntaxException e) {
var uri = exc.getOriginalRelativeURI();
if (e.getIndex() >= 0 && e.getIndex() < uri.length()) {
Expand All @@ -106,22 +115,23 @@ private String getForwardingDestination(Exchange exc) throws Exception {
}

protected String getAddressFromTargetElement(Exchange exc) throws MalformedURLException, URISyntaxException {
AbstractServiceProxy p = (AbstractServiceProxy) exc.getProxy();
if (!(exc.getProxy() instanceof AbstractServiceProxy asp))
return null;

if (p.getTargetURL() != null) {
String targetURL = p.getTarget().compileUrl(exc, REQUEST);
if (asp.getTargetURL() != null) {
var targetURL = asp.getTargetURL();
if (targetURL.startsWith("http") || targetURL.startsWith("internal")) {
String basePath = UriUtil.getPathFromURL(router.getConfiguration().getUriFactory(), targetURL);
// Here illegal character as $ { } are allowed in the URI to make URL expressions possible.
// The URL is from the target in the configuration, maintained by admin
var basePath = UriUtil.getPathFromURL(ALLOW_ILLEGAL_CHARACTERS_URI_FACTORY, targetURL);
if (basePath == null || basePath.isEmpty() || "/".equals(basePath)) {
URI base = new URI(targetURL);
// Resolve and normalize slashes consistently with the branch below.
return base.resolve(getUri(exc)).toString();
return combine(router.getConfiguration().getUriFactory(),targetURL,getUri(exc));
}
}
return targetURL;
}
if (p.getTargetHost() != null) {
return new URL(p.getTargetScheme(), p.getTargetHost(), p.getTargetPort(), getUri(exc)).toString();
if (asp.getTargetHost() != null) {
return new URL(asp.getTargetScheme(), asp.getTargetHost(), asp.getTargetPort(), getUri(exc)).toString();
}

// That's fine. Maybe it is a <soapProxy> without a target
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,19 @@
limitations under the License. */
package com.predic8.membrane.core.interceptor;

import com.predic8.membrane.annot.MCAttribute;
import com.predic8.membrane.annot.MCChildElement;
import com.predic8.membrane.annot.MCElement;
import com.predic8.membrane.core.exchange.Exchange;
import com.predic8.membrane.core.http.EmptyBody;
import com.predic8.membrane.core.http.Request;
import com.predic8.membrane.core.proxies.AbstractServiceProxy;
import com.predic8.membrane.core.transport.http.HttpClient;
import com.predic8.membrane.core.transport.http.ProtocolUpgradeDeniedException;
import com.predic8.membrane.core.transport.http.client.HttpClientConfiguration;
import com.predic8.membrane.core.util.URLUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import com.predic8.membrane.annot.*;
import com.predic8.membrane.core.exchange.*;
import com.predic8.membrane.core.proxies.*;
import com.predic8.membrane.core.transport.http.*;
import com.predic8.membrane.core.transport.http.client.*;
import com.predic8.membrane.core.util.*;
import org.slf4j.*;

import java.net.*;

import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
import static com.predic8.membrane.core.http.Header.*;
import static com.predic8.membrane.core.http.Request.METHOD_GET;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.Set.REQUEST_FLOW;
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
import static com.predic8.membrane.core.interceptor.Outcome.RETURN;
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.Set.*;
import static com.predic8.membrane.core.interceptor.Outcome.*;

/**
* @description The <i>httpClient</i> sends the request of an exchange to a Web
Expand All @@ -48,16 +35,15 @@
* its outgoing HTTP connection that is different from the global
* configuration in the transport.
*/
@MCElement(name = "httpClient", excludeFromFlow= true)
@MCElement(name = "httpClient", excludeFromFlow = true)
public class HTTPClientInterceptor extends AbstractInterceptor {

private static final Logger log = LoggerFactory.getLogger(HTTPClientInterceptor.class.getName());

private static final String PROXIES_HINT = " Maybe the target is only reachable over an HTTP proxy server. Please check proxy settings in conf/proxies.xml.";
private static final String PROXIES_HINT = " Maybe the target is only reachable over an HTTP proxy server. Please check proxy settings in apis.yaml or proxies.xml.";

// null => inherit from HttpClientConfiguration unless explicitly set here
private Boolean failOverOn5XX;
private Boolean adjustHostHeader;

private HttpClientConfiguration httpClientConfig = new HttpClientConfiguration();

Expand All @@ -77,24 +63,19 @@ public void init() {
httpClientConfig.getRetryHandler().setFailOverOn5XX(failOverOn5XX);
}

if (adjustHostHeader != null) {
httpClientConfig.setAdjustHostHeader(adjustHostHeader);
}

hc = router.getHttpClientFactory().createClient(httpClientConfig);
hc.setStreamPumpStats(getRouter().getStatistics().getStreamPumpStats());
}

@Override
public Outcome handleRequest(Exchange exc) {
changeMethod(exc);

try {
applyTargetModifications(exc);
hc.call(exc);
return RETURN;
} catch (ConnectException e) {
String msg = "Target %s is not reachable.".formatted(getDestination(exc));
log.warn(msg + PROXIES_HINT);
log.warn("{} {}",msg,PROXIES_HINT);
gateway(router.getConfiguration().isProduction(), getDisplayName())
.addSubSee("connect")
.status(502)
Expand All @@ -109,22 +90,22 @@ public Outcome handleRequest(Exchange exc) {
.buildAndSetResponse(exc);
return ABORT;
} catch (UnknownHostException e) {
String msg = "Target host %s of API %s is unknown. DNS was unable to resolve host name.".formatted(URLUtil.getHost(getDestination(exc)), exc.getProxy().getName());
log.warn(msg + PROXIES_HINT);
String msg = "Target host %s of API %s is unknown. DNS was unable to resolve host name.".formatted(URLUtil.getAuthority(getDestination(exc)), exc.getProxy().getName());
log.warn("{} {}",msg,PROXIES_HINT);
gateway(router.getConfiguration().isProduction(), getDisplayName())
.addSubSee("unknown-host")
.status(502)
.detail(msg)
.buildAndSetResponse(exc);
return ABORT;
} catch (MalformedURLException e) {
log.info("Malformed URL. Requested path is: {} {}",exc.getRequest().getUri() , e.getMessage());
log.debug("",e);
log.info("Malformed URL. Requested path is: {} {}", exc.getRequest().getUri(), e.getMessage());
log.debug("", e);
user(router.getConfiguration().isProduction(), getDisplayName())
.title("Request path or 'Host' header is malformed")
.addSubSee("malformed-url")
.internal("proxy", exc.getProxy().getName())
.internal("url",exc.getRequest().getUri())
.internal("url", exc.getRequest().getUri())
.internal("hostHeader", exc.getRequest().getHeader().getHost())
.detail(e.getMessage())
.buildAndSetResponse(exc);
Expand All @@ -137,7 +118,7 @@ public Outcome handleRequest(Exchange exc) {
.addSubSee("denied-protocol-upgrade")
.internal("hint", "Protocol upgrades are supported by Membrane for 'websocket' and 'tcp', but have to be allowed in the configuration explicitly.")
.internal("proxy", exc.getProxy().getName())
.internal("url",exc.getRequest().getUri())
.internal("url", exc.getRequest().getUri())
.buildAndSetResponse(exc);
return ABORT;
} catch (Exception e) {
Expand All @@ -151,36 +132,15 @@ public Outcome handleRequest(Exchange exc) {
}

/**
* Makes it possible to change the method by specifying <target method="POST"/>
* Manipulates the target URL according to the target (change of method, URL expression)
*
* @param exc
*/
private static void changeMethod(Exchange exc) {
void applyTargetModifications(Exchange exc) {
if (!(exc.getProxy() instanceof AbstractServiceProxy asp) || asp.getTarget() == null)
return;

String newMethod = asp.getTarget().getMethod();
if (newMethod == null || newMethod.equalsIgnoreCase(exc.getRequest().getMethod()))
return;

log.debug("Changing method from {} to {}", exc.getRequest().getMethod(), newMethod);
exc.getRequest().setMethod(newMethod);

if (newMethod.equalsIgnoreCase(METHOD_GET)) {
handleBodyContentWhenChangingToGET(exc);
}
}

private static void handleBodyContentWhenChangingToGET(Exchange exc) {
Request req = exc.getRequest();
try {
req.readBody();
} catch (IOException ignored) {
}
req.setBody(new EmptyBody());
req.getHeader().removeFields(CONTENT_LENGTH);
req.getHeader().removeFields(CONTENT_TYPE);
req.getHeader().removeFields(CONTENT_ENCODING);
asp.getTarget().applyModifications(exc, getRouter());
}

private String getDestination(Exchange exc) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ private boolean isTargetInternal(Exchange exc) {
}

private AbstractServiceProxy getRuleByDest(Exchange exchange) {
Proxy proxy = router.getRuleManager().getRuleByName(getHost(exchange.getDestinations().getFirst()), Proxy.class);
Proxy proxy = router.getRuleManager().getRuleByName(getAuthority(exchange.getDestinations().getFirst()), Proxy.class);
if (proxy == null)
throw new RuntimeException("No api found for destination " + exchange.getDestinations().getFirst());
if (proxy instanceof AbstractServiceProxy sp)
Expand Down
Loading
Loading