diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f37b9c65d..b6b876985d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,26 @@
## [Unreleased](https://github.com/aklivity/zilla/tree/HEAD)
-[Full Changelog](https://github.com/aklivity/zilla/compare/0.9.56...HEAD)
+[Full Changelog](https://github.com/aklivity/zilla/compare/0.9.57...HEAD)
+
+**Implemented enhancements:**
+
+- Integrate `http` binding with `validators` [\#455](https://github.com/aklivity/zilla/issues/455)
+
+**Fixed bugs:**
+
+- \[MQTT-Kafka\] Exception runtime.binding.mqtt.kafka.internal.types.MqttExpirySignalFW.wrap\(MqttExpirySignalFW.java:45\) [\#563](https://github.com/aklivity/zilla/issues/563)
+- Running mqtt benchmark triggers mqtt exception [\#488](https://github.com/aklivity/zilla/issues/488)
+- Fix IndexOutOfBoundsException when receiving expiry signal [\#567](https://github.com/aklivity/zilla/pull/567) ([bmaidics](https://github.com/bmaidics))
+
+**Merged pull requests:**
+
+- Integrate http binding with validators [\#571](https://github.com/aklivity/zilla/pull/571) ([attilakreiner](https://github.com/attilakreiner))
+- Fix flow conrol bug + indexoutofbound exception [\#568](https://github.com/aklivity/zilla/pull/568) ([bmaidics](https://github.com/bmaidics))
+
+## [0.9.57](https://github.com/aklivity/zilla/tree/0.9.57) (2023-11-04)
+
+[Full Changelog](https://github.com/aklivity/zilla/compare/0.9.56...0.9.57)
**Fixed bugs:**
diff --git a/README.md b/README.md
index 9f4face008..324b6a4c6b 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ When Zilla is deployed alongside Apache Kafka®, any application or service can
- [REST-Kafka Proxying](#rest-kafka-proxying)
- [SSE-Kafka Proxying](#sse-kafka-proxying)
- [gRPC-Kafka Proxying](#grpc-kafka-proxying)
- - [MQTT-Kafka Proxying]((#mqtt-kafka-proxying))
+ - [MQTT-Kafka Proxying](#mqtt-kafka-proxying)
- [Resources](#resources)
- [How Zilla Works](#how-zilla-works)
- [FAQs](#faqs)
diff --git a/build/flyweight-maven-plugin/pom.xml b/build/flyweight-maven-plugin/pom.xml
index 4c9c4a0513..31e6a0bf8f 100644
--- a/build/flyweight-maven-plugin/pom.xml
+++ b/build/flyweight-maven-plugin/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
build
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/build/pom.xml b/build/pom.xml
index 7e423ff2c5..7ea2f4a017 100644
--- a/build/pom.xml
+++ b/build/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/cloud/docker-image/pom.xml b/cloud/docker-image/pom.xml
index dca796ab69..bcbccbbee8 100644
--- a/cloud/docker-image/pom.xml
+++ b/cloud/docker-image/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
cloud
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/cloud/helm-chart/pom.xml b/cloud/helm-chart/pom.xml
index 2bb31009bf..3598dbbdee 100644
--- a/cloud/helm-chart/pom.xml
+++ b/cloud/helm-chart/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
cloud
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/cloud/pom.xml b/cloud/pom.xml
index 8ca69d9a92..95b2b7be93 100644
--- a/cloud/pom.xml
+++ b/cloud/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/conf/pom.xml b/conf/pom.xml
index 82dd874a8b..2432643772 100644
--- a/conf/pom.xml
+++ b/conf/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/binding-amqp.spec/pom.xml b/incubator/binding-amqp.spec/pom.xml
index 6c658f2292..c8b0480a15 100644
--- a/incubator/binding-amqp.spec/pom.xml
+++ b/incubator/binding-amqp.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/binding-amqp/pom.xml b/incubator/binding-amqp/pom.xml
index a7192a7558..5fa617061f 100644
--- a/incubator/binding-amqp/pom.xml
+++ b/incubator/binding-amqp/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/catalog-inline.spec/pom.xml b/incubator/catalog-inline.spec/pom.xml
index cebdcc0fe3..b35c8f91b4 100644
--- a/incubator/catalog-inline.spec/pom.xml
+++ b/incubator/catalog-inline.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/catalog-inline/pom.xml b/incubator/catalog-inline/pom.xml
index 5d804b362f..4057920616 100644
--- a/incubator/catalog-inline/pom.xml
+++ b/incubator/catalog-inline/pom.xml
@@ -6,7 +6,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/catalog-schema-registry.spec/pom.xml b/incubator/catalog-schema-registry.spec/pom.xml
index 55975f1f22..8dd0d7e197 100644
--- a/incubator/catalog-schema-registry.spec/pom.xml
+++ b/incubator/catalog-schema-registry.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/catalog-schema-registry/pom.xml b/incubator/catalog-schema-registry/pom.xml
index 312505713e..bf4a7ba02a 100644
--- a/incubator/catalog-schema-registry/pom.xml
+++ b/incubator/catalog-schema-registry/pom.xml
@@ -6,7 +6,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/command-dump/pom.xml b/incubator/command-dump/pom.xml
index 490cdf34d7..1d6805e2bb 100644
--- a/incubator/command-dump/pom.xml
+++ b/incubator/command-dump/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/command-generate/pom.xml b/incubator/command-generate/pom.xml
index 4804020de3..1b5e772743 100644
--- a/incubator/command-generate/pom.xml
+++ b/incubator/command-generate/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/command-log/pom.xml b/incubator/command-log/pom.xml
index 41887129d4..98133854c9 100644
--- a/incubator/command-log/pom.xml
+++ b/incubator/command-log/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/command-tune/pom.xml b/incubator/command-tune/pom.xml
index 7cff3b2326..29b1815461 100644
--- a/incubator/command-tune/pom.xml
+++ b/incubator/command-tune/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/exporter-otlp.spec/pom.xml b/incubator/exporter-otlp.spec/pom.xml
index bf2150f476..e6fae67081 100644
--- a/incubator/exporter-otlp.spec/pom.xml
+++ b/incubator/exporter-otlp.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/exporter-otlp/pom.xml b/incubator/exporter-otlp/pom.xml
index b68e5855c0..ab1062ee58 100644
--- a/incubator/exporter-otlp/pom.xml
+++ b/incubator/exporter-otlp/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/pom.xml b/incubator/pom.xml
index e1fd785390..89295b4b09 100644
--- a/incubator/pom.xml
+++ b/incubator/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-avro.spec/pom.xml b/incubator/validator-avro.spec/pom.xml
index df003773c5..6fb3910952 100644
--- a/incubator/validator-avro.spec/pom.xml
+++ b/incubator/validator-avro.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-avro/pom.xml b/incubator/validator-avro/pom.xml
index 6d20550c48..ac48daa880 100644
--- a/incubator/validator-avro/pom.xml
+++ b/incubator/validator-avro/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-core.spec/pom.xml b/incubator/validator-core.spec/pom.xml
index 1cc3a59c23..fb36f493af 100644
--- a/incubator/validator-core.spec/pom.xml
+++ b/incubator/validator-core.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-core/pom.xml b/incubator/validator-core/pom.xml
index 44bd289ddb..20f0ffb89a 100644
--- a/incubator/validator-core/pom.xml
+++ b/incubator/validator-core/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-json.spec/pom.xml b/incubator/validator-json.spec/pom.xml
index 6e9f0c6f2c..b39c3eab8c 100644
--- a/incubator/validator-json.spec/pom.xml
+++ b/incubator/validator-json.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/incubator/validator-json/pom.xml b/incubator/validator-json/pom.xml
index f059036bf8..ca82a65d49 100644
--- a/incubator/validator-json/pom.xml
+++ b/incubator/validator-json/pom.xml
@@ -6,7 +6,7 @@
io.aklivity.zilla
incubator
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/manager/pom.xml b/manager/pom.xml
index ffd39ff674..e1bf2e8831 100644
--- a/manager/pom.xml
+++ b/manager/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/pom.xml b/pom.xml
index f71cf47830..a292f3bb81 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
4.0.0
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
pom
zilla
https://github.com/aklivity/zilla
diff --git a/runtime/binding-echo/pom.xml b/runtime/binding-echo/pom.xml
index b6dac2889d..e511abb7cd 100644
--- a/runtime/binding-echo/pom.xml
+++ b/runtime/binding-echo/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-fan/pom.xml b/runtime/binding-fan/pom.xml
index 24c15623b6..dbe707a835 100644
--- a/runtime/binding-fan/pom.xml
+++ b/runtime/binding-fan/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-filesystem/pom.xml b/runtime/binding-filesystem/pom.xml
index 65fe10297f..0ef11ab9da 100644
--- a/runtime/binding-filesystem/pom.xml
+++ b/runtime/binding-filesystem/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-grpc-kafka/pom.xml b/runtime/binding-grpc-kafka/pom.xml
index bd590c0763..47e55e8fba 100644
--- a/runtime/binding-grpc-kafka/pom.xml
+++ b/runtime/binding-grpc-kafka/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-grpc/pom.xml b/runtime/binding-grpc/pom.xml
index 4e208474f4..e8c841fc25 100644
--- a/runtime/binding-grpc/pom.xml
+++ b/runtime/binding-grpc/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-http-filesystem/pom.xml b/runtime/binding-http-filesystem/pom.xml
index 25296f739b..510e883ea5 100644
--- a/runtime/binding-http-filesystem/pom.xml
+++ b/runtime/binding-http-filesystem/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-http-kafka/pom.xml b/runtime/binding-http-kafka/pom.xml
index fa1cb0b715..c36b7970eb 100644
--- a/runtime/binding-http-kafka/pom.xml
+++ b/runtime/binding-http-kafka/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-http/pom.xml b/runtime/binding-http/pom.xml
index bef699cce8..ac6833acbd 100644
--- a/runtime/binding-http/pom.xml
+++ b/runtime/binding-http/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
@@ -137,7 +137,7 @@
- io/aklivity/zilla/specs/binding/http/schema/http*.schema.patch.json
+ io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json
${project.build.directory}/classes
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpRequestConfig.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpRequestConfig.java
index 879f3dfb8b..f8b97422da 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpRequestConfig.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/config/HttpRequestConfig.java
@@ -35,13 +35,13 @@ public enum Method
TRACE
}
- public String path;
- public Method method;
- public List contentType;
- public List headers;
- public List pathParams;
- public List queryParams;
- public ValidatorConfig content;
+ public final String path;
+ public final Method method;
+ public final List contentType;
+ public final List headers;
+ public final List pathParams;
+ public final List queryParams;
+ public final ValidatorConfig content;
public HttpRequestConfig(
String path,
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpBinding.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpBinding.java
index e020b8c536..f274b56bd5 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpBinding.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpBinding.java
@@ -41,10 +41,7 @@ public String name()
@Override
public URL type()
{
- String patch = config.requestValidators()
- ? "schema/http.with.validators.schema.patch.json"
- : "schema/http.schema.patch.json";
- return getClass().getResource(patch);
+ return getClass().getResource("schema/http.schema.patch.json");
}
@Override
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpConfiguration.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpConfiguration.java
index 81c5b7b571..58df0ec1cd 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpConfiguration.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/HttpConfiguration.java
@@ -35,7 +35,6 @@ public class HttpConfiguration extends Configuration
public static final IntPropertyDef HTTP_MAX_CONCURRENT_APPLICATION_HEADERS;
public static final PropertyDef HTTP_SERVER_HEADER;
public static final PropertyDef HTTP_USER_AGENT_HEADER;
- public static final BooleanPropertyDef HTTP_REQUEST_VALIDATORS;
private static final ConfigurationDef HTTP_CONFIG;
@@ -53,7 +52,6 @@ public class HttpConfiguration extends Configuration
HTTP_MAX_CONCURRENT_STREAMS_CLEANUP = config.property("max.concurrent.streams.cleanup", 1000);
HTTP_STREAMS_CLEANUP_DELAY = config.property("streams.cleanup.delay", 100);
HTTP_MAX_CONCURRENT_APPLICATION_HEADERS = config.property("max.concurrent.application.headers", 10000);
- HTTP_REQUEST_VALIDATORS = config.property("request.validators", false);
HTTP_CONFIG = config;
}
@@ -115,11 +113,6 @@ public int maxConcurrentApplicationHeaders()
return HTTP_MAX_CONCURRENT_APPLICATION_HEADERS.getAsInt(this);
}
- public boolean requestValidators()
- {
- return HTTP_REQUEST_VALIDATORS.getAsBoolean(this);
- }
-
public String16FW serverHeader()
{
return serverHeader;
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpBindingConfig.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpBindingConfig.java
index 9bbe39aa86..74936c27c3 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpBindingConfig.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpBindingConfig.java
@@ -19,21 +19,38 @@
import static java.util.EnumSet.allOf;
import static java.util.stream.Collectors.toList;
+import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.SortedSet;
+import java.util.TreeMap;
import java.util.TreeSet;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.ToLongFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.agrona.DirectBuffer;
+import org.agrona.collections.MutableBoolean;
+import org.agrona.collections.Object2ObjectHashMap;
+
import io.aklivity.zilla.runtime.binding.http.config.HttpAccessControlConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpCredentialsConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpOptionsConfig;
+import io.aklivity.zilla.runtime.binding.http.config.HttpParamConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpPatternConfig;
+import io.aklivity.zilla.runtime.binding.http.config.HttpRequestConfig;
import io.aklivity.zilla.runtime.binding.http.config.HttpVersion;
+import io.aklivity.zilla.runtime.binding.http.internal.types.HttpHeaderFW;
+import io.aklivity.zilla.runtime.binding.http.internal.types.String16FW;
+import io.aklivity.zilla.runtime.binding.http.internal.types.String8FW;
+import io.aklivity.zilla.runtime.binding.http.internal.types.stream.HttpBeginExFW;
import io.aklivity.zilla.runtime.engine.config.BindingConfig;
import io.aklivity.zilla.runtime.engine.config.KindConfig;
+import io.aklivity.zilla.runtime.engine.config.ValidatorConfig;
+import io.aklivity.zilla.runtime.engine.validator.Validator;
public final class HttpBindingConfig
{
@@ -41,6 +58,10 @@ public final class HttpBindingConfig
private static final SortedSet DEFAULT_VERSIONS = new TreeSet<>(allOf(HttpVersion.class));
private static final HttpAccessControlConfig DEFAULT_ACCESS_CONTROL =
HttpAccessControlConfig.builder().policy(SAME_ORIGIN).build();
+ private static final String8FW HEADER_CONTENT_TYPE = new String8FW("content-type");
+ private static final String8FW HEADER_METHOD = new String8FW(":method");
+ private static final String8FW HEADER_PATH = new String8FW(":path");
+ private static final HttpQueryStringComparator QUERY_STRING_COMPARATOR = new HttpQueryStringComparator();
public final long id;
public final String name;
@@ -49,9 +70,17 @@ public final class HttpBindingConfig
public final List routes;
public final ToLongFunction resolveId;
public final Function, String> credentials;
+ public final List requests;
public HttpBindingConfig(
BindingConfig binding)
+ {
+ this(binding, null);
+ }
+
+ public HttpBindingConfig(
+ BindingConfig binding,
+ BiFunction, Validator> createValidator)
{
this.id = binding.id;
this.name = binding.name;
@@ -61,6 +90,7 @@ public HttpBindingConfig(
this.resolveId = binding.resolveId;
this.credentials = options != null && options.authorization != null ?
asAccessor(options.authorization.credentials) : DEFAULT_CREDENTIALS;
+ this.requests = createValidator == null ? null : createRequestTypes(createValidator);
}
public HttpRouteConfig resolve(
@@ -164,6 +194,188 @@ private Function, String> asAccessor(
return accessor;
}
+ private List createRequestTypes(
+ BiFunction, Validator> createValidator)
+ {
+ List requestTypes = new LinkedList<>();
+ if (this.options != null && this.options.requests != null)
+ {
+ for (HttpRequestConfig request : this.options.requests)
+ {
+ Map headers = new HashMap<>();
+ if (request.headers != null)
+ {
+ for (HttpParamConfig header : request.headers)
+ {
+ headers.put(new String8FW(header.name), createValidator.apply(header.validator, this.resolveId));
+ }
+ }
+ Map pathParams = new Object2ObjectHashMap<>();
+ if (request.pathParams != null)
+ {
+ for (HttpParamConfig pathParam : request.pathParams)
+ {
+ pathParams.put(pathParam.name, createValidator.apply(pathParam.validator, this.resolveId));
+ }
+ }
+ Map queryParams = new TreeMap<>(QUERY_STRING_COMPARATOR);
+ if (request.queryParams != null)
+ {
+ for (HttpParamConfig queryParam : request.queryParams)
+ {
+ queryParams.put(queryParam.name, createValidator.apply(queryParam.validator, this.resolveId));
+ }
+ }
+ Validator content = createValidator.apply(request.content, this.resolveId);
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(request.path)
+ .method(request.method)
+ .contentType(request.contentType)
+ .headers(headers)
+ .pathParams(pathParams)
+ .queryParams(queryParams)
+ .content(content)
+ .build();
+ requestTypes.add(requestType);
+ }
+ }
+ return requestTypes;
+ }
+
+ public HttpRequestType resolveRequestType(
+ HttpBeginExFW beginEx)
+ {
+ HttpRequestType result = null;
+ if (requests != null && !requests.isEmpty())
+ {
+ String path = resolveHeaderValue(beginEx, HEADER_PATH);
+ String method = resolveHeaderValue(beginEx, HEADER_METHOD);
+ String contentType = resolveHeaderValue(beginEx, HEADER_CONTENT_TYPE);
+ for (HttpRequestType requestType : requests)
+ {
+ if (matchMethod(requestType, method) &&
+ matchContentType(requestType, contentType) &&
+ matchPath(requestType, path))
+ {
+ result = requestType;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private boolean matchMethod(
+ HttpRequestType requestType,
+ String method)
+ {
+ return method == null || requestType.method == null || method.equals(requestType.method.name());
+ }
+
+ private boolean matchContentType(
+ HttpRequestType requestType,
+ String contentType)
+ {
+ return contentType == null || requestType.contentType == null || requestType.contentType.contains(contentType);
+ }
+
+ private boolean matchPath(
+ HttpRequestType requestType,
+ String path)
+ {
+ return requestType.pathMatcher.reset(path).matches();
+ }
+
+ public boolean validateHeaders(
+ HttpRequestType requestType,
+ HttpBeginExFW beginEx)
+ {
+ String path = beginEx.headers().matchFirst(h -> h.name().equals(HEADER_PATH)).value().asString();
+ return requestType == null ||
+ validateHeaderValues(requestType, beginEx) &&
+ validatePathParams(requestType, path) &&
+ validateQueryParams(requestType, path);
+ }
+
+ private boolean validateHeaderValues(
+ HttpRequestType requestType,
+ HttpBeginExFW beginEx)
+ {
+ MutableBoolean valid = new MutableBoolean(true);
+ if (requestType != null && requestType.headers != null)
+ {
+ beginEx.headers().forEach(header ->
+ {
+ if (valid.value)
+ {
+ Validator validator = requestType.headers.get(header.name());
+ if (validator != null)
+ {
+ String16FW value = header.value();
+ valid.value &= validator.read(value.value(), value.offset(), value.length());
+ }
+ }
+ });
+ }
+ return valid.value;
+ }
+
+ private boolean validatePathParams(
+ HttpRequestType requestType,
+ String path)
+ {
+ Matcher matcher = requestType.pathMatcher.reset(path);
+ boolean matches = matcher.matches();
+ assert matches;
+
+ boolean valid = true;
+ for (String name : requestType.pathParams.keySet())
+ {
+ String value = matcher.group(name);
+ if (value != null)
+ {
+ String8FW value0 = new String8FW(value);
+ Validator validator = requestType.pathParams.get(name);
+ if (!validator.read(value0.value(), value0.offset(), value0.length()))
+ {
+ valid = false;
+ break;
+ }
+ }
+ }
+ return valid;
+ }
+
+ private boolean validateQueryParams(
+ HttpRequestType requestType,
+ String path)
+ {
+ Matcher matcher = requestType.queryMatcher.reset(path);
+ boolean valid = true;
+ while (valid && matcher.find())
+ {
+ String name = matcher.group(1);
+ Validator validator = requestType.queryParams.get(name);
+ if (validator != null)
+ {
+ String8FW value = new String8FW(matcher.group(2));
+ valid &= validator.read(value.value(), value.offset(), value.length());
+ }
+ }
+ return valid;
+ }
+
+ public boolean validateContent(
+ HttpRequestType requestType,
+ DirectBuffer buffer,
+ int index,
+ int length)
+ {
+ return requestType == null ||
+ requestType.content == null ||
+ requestType.content.read(buffer, index, length);
+ }
+
private static Function, String> orElseIfNull(
Function, String> first,
Function, String> second)
@@ -174,4 +386,17 @@ private static Function, String> orElseIfNull(
return result != null ? result : second.apply(hs);
};
}
+
+ private static String resolveHeaderValue(
+ HttpBeginExFW beginEx,
+ String8FW headerName)
+ {
+ String result = null;
+ HttpHeaderFW header = beginEx.headers().matchFirst(h -> headerName.equals(h.name()));
+ if (header != null)
+ {
+ result = header.value().asString();
+ }
+ return result;
+ }
}
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparator.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparator.java
new file mode 100644
index 0000000000..2e904069fd
--- /dev/null
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.config;
+
+import java.util.Comparator;
+
+public class HttpQueryStringComparator implements Comparator
+{
+ @Override
+ public int compare(
+ String input1,
+ String input2)
+ {
+ int result = 0;
+ int index1 = 0;
+ int index2 = 0;
+
+ while (hasNext(input1, index1) && hasNext(input2, index2))
+ {
+ boolean percent1 = percent(input1, index1);
+ boolean percent2 = percent(input2, index2);
+ char char1 = charAt(input1, index1, percent1);
+ char char2 = charAt(input2, index2, percent2);
+
+ if (char1 != char2)
+ {
+ result = Character.compare(char1, char2);
+ break;
+ }
+
+ index1 = nextIndex(index1, percent1);
+ index2 = nextIndex(index2, percent2);
+ }
+
+ return result == 0 ? Boolean.compare(hasNext(input1, index1), hasNext(input2, index2)) : result;
+ }
+
+ private boolean hasNext(
+ String input,
+ int index)
+ {
+ return index < input.length();
+ }
+
+ private boolean percent(
+ String input,
+ int index)
+ {
+ return input.charAt(index) == '%' && index + 3 <= input.length();
+ }
+
+ private char charAt(
+ String input,
+ int index,
+ boolean percent)
+ {
+ char result;
+ if (percent)
+ {
+ char hexDigit1 = input.charAt(index + 1);
+ char hexDigit2 = input.charAt(index + 2);
+ result = toChar(hexDigit1, hexDigit2);
+ }
+ else
+ {
+ result = input.charAt(index);
+ }
+ return result;
+ }
+
+ private char toChar(
+ char hexDigit1,
+ char hexDigit2)
+ {
+ int int1 = Character.digit(hexDigit1, 16);
+ int int2 = Character.digit(hexDigit2, 16);
+ return (char) ((int1 << 4) | int2);
+ }
+
+ private int nextIndex(
+ int index,
+ boolean percent)
+ {
+ return index + (percent ? 3 : 1);
+ }
+}
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestType.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestType.java
new file mode 100644
index 0000000000..0b4386deca
--- /dev/null
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestType.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.config;
+
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.aklivity.zilla.runtime.binding.http.config.HttpRequestConfig;
+import io.aklivity.zilla.runtime.binding.http.internal.types.String8FW;
+import io.aklivity.zilla.runtime.engine.validator.Validator;
+
+public final class HttpRequestType
+{
+ private static final String PATH_FORMAT = "^%s/?(?:\\?.*)?$";
+ private static final String PATH_REGEX = "\\{([a-zA-Z0-9_-]+)\\}";
+ private static final String PATH_REPLACEMENT = "(?<$1>.+?)";
+ private static final String QUERY_REGEX = "(?<=[?&])([^&=]+)=([^&]+)(?=&|$)";
+ private static final Pattern QUERY_PATTERN = Pattern.compile(QUERY_REGEX);
+ private static final String EMPTY_INPUT = "";
+
+ // selectors
+ public final String path;
+ public final HttpRequestConfig.Method method;
+ public final List contentType;
+
+ // matchers
+ public final Matcher pathMatcher;
+ public final Matcher queryMatcher;
+
+ // validators
+ public final Map headers;
+ public final Map pathParams;
+ public final Map queryParams;
+ public final Validator content;
+
+ private HttpRequestType(
+ String path,
+ HttpRequestConfig.Method method,
+ List contentType,
+ Matcher pathMatcher,
+ Matcher queryMatcher,
+ Map headers,
+ Map pathParams,
+ Map queryParams,
+ Validator content)
+ {
+ this.path = path;
+ this.method = method;
+ this.contentType = contentType;
+ this.pathMatcher = pathMatcher;
+ this.queryMatcher = queryMatcher;
+ this.headers = headers;
+ this.pathParams = pathParams;
+ this.queryParams = queryParams;
+ this.content = content;
+ }
+
+ public static Builder builder()
+ {
+ return new Builder();
+ }
+
+ public static final class Builder
+ {
+ private String path;
+ private HttpRequestConfig.Method method;
+ private List contentType;
+ private Map headers;
+ private Map pathParams;
+ private Map queryParams;
+ private Validator content;
+
+ public Builder path(
+ String path)
+ {
+ this.path = path;
+ return this;
+ }
+
+ public Builder method(
+ HttpRequestConfig.Method method)
+ {
+ this.method = method;
+ return this;
+ }
+
+ public Builder contentType(
+ List contentType)
+ {
+ this.contentType = contentType;
+ return this;
+ }
+
+ public Builder headers(
+ Map headers)
+ {
+ this.headers = headers;
+ return this;
+ }
+
+ public Builder pathParams(
+ Map pathParams)
+ {
+ this.pathParams = pathParams;
+ return this;
+ }
+
+ public Builder queryParams(
+ Map queryParams)
+ {
+ this.queryParams = queryParams;
+ return this;
+ }
+
+ public Builder content(
+ Validator content)
+ {
+ this.content = content;
+ return this;
+ }
+
+ public HttpRequestType build()
+ {
+ String pathPattern = String.format(PATH_FORMAT, path.replaceAll(PATH_REGEX, PATH_REPLACEMENT));
+ Matcher pathMatcher = Pattern.compile(pathPattern).matcher(EMPTY_INPUT);
+ Matcher queryMatcher = QUERY_PATTERN.matcher(EMPTY_INPUT);
+ return new HttpRequestType(path, method, contentType, pathMatcher, queryMatcher, headers, pathParams, queryParams,
+ content);
+ }
+ }
+}
diff --git a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/stream/HttpServerFactory.java b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/stream/HttpServerFactory.java
index d30efeeee2..c713fb5374 100644
--- a/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/stream/HttpServerFactory.java
+++ b/runtime/binding-http/src/main/java/io/aklivity/zilla/runtime/binding/http/internal/stream/HttpServerFactory.java
@@ -57,11 +57,13 @@
import java.util.Set;
import java.util.SortedSet;
import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongFunction;
import java.util.function.LongSupplier;
import java.util.function.LongUnaryOperator;
+import java.util.function.ToLongFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -100,6 +102,7 @@
import io.aklivity.zilla.runtime.binding.http.internal.codec.Http2SettingsFW;
import io.aklivity.zilla.runtime.binding.http.internal.codec.Http2WindowUpdateFW;
import io.aklivity.zilla.runtime.binding.http.internal.config.HttpBindingConfig;
+import io.aklivity.zilla.runtime.binding.http.internal.config.HttpRequestType;
import io.aklivity.zilla.runtime.binding.http.internal.config.HttpRouteConfig;
import io.aklivity.zilla.runtime.binding.http.internal.hpack.HpackContext;
import io.aklivity.zilla.runtime.binding.http.internal.hpack.HpackHeaderBlockFW;
@@ -139,7 +142,9 @@
import io.aklivity.zilla.runtime.engine.buffer.BufferPool;
import io.aklivity.zilla.runtime.engine.concurrent.Signaler;
import io.aklivity.zilla.runtime.engine.config.BindingConfig;
+import io.aklivity.zilla.runtime.engine.config.ValidatorConfig;
import io.aklivity.zilla.runtime.engine.guard.GuardHandler;
+import io.aklivity.zilla.runtime.engine.validator.Validator;
public final class HttpServerFactory implements HttpStreamFactory
{
@@ -401,6 +406,7 @@ public final class HttpServerFactory implements HttpStreamFactory
private final Array32FW headers400;
private final Array32FW headers403;
private final Array32FW headers404;
+ private final DirectBuffer response400;
private final DirectBuffer response403;
private final DirectBuffer response404;
@@ -494,6 +500,8 @@ public final class HttpServerFactory implements HttpStreamFactory
private final Http2ServerDecoder decodeHttp2IgnoreAll = this::decodeHttp2IgnoreAll;
private final EnumMap decodersByFrameType;
+ private final BiFunction, Validator> createValidator;
+
{
final EnumMap decodersByFrameType = new EnumMap<>(Http2FrameType.class);
decodersByFrameType.put(Http2FrameType.SETTINGS, decodeHttp2Settings);
@@ -566,6 +574,7 @@ public HttpServerFactory(
this.connectionClose = CONNECTION_CLOSE_PATTERN.matcher("");
this.maximumHeadersSize = bufferPool.slotCapacity();
this.decodeMax = bufferPool.slotCapacity();
+ this.createValidator = context::createValidator;
this.encodeMax = bufferPool.slotCapacity();
this.bindings = new Long2ObjectHashMap<>();
@@ -574,6 +583,7 @@ public HttpServerFactory(
this.headers400 = initHeadersEmpty(config, STATUS_400);
this.headers403 = initHeaders(config, STATUS_403);
this.headers404 = initHeadersEmpty(config, STATUS_404);
+ this.response400 = initResponse(config, 400, "Bad Request");
this.response403 = initResponse(config, 403, "Forbidden");
this.response404 = initResponse(config, 404, "Not Found");
}
@@ -588,7 +598,7 @@ public int routedTypeId()
public void attach(
BindingConfig binding)
{
- HttpBindingConfig httpBinding = new HttpBindingConfig(binding);
+ HttpBindingConfig httpBinding = new HttpBindingConfig(binding, createValidator);
bindings.put(binding.id, httpBinding);
}
@@ -1066,7 +1076,13 @@ else if (!isCorsRequestAllowed(server.binding, headers))
HttpPolicyConfig policy = binding.access().effectivePolicy(headers);
final String origin = policy == CROSS_ORIGIN ? headers.get(HEADER_NAME_ORIGIN) : null;
- server.onDecodeHeaders(server.routedId, route.id, traceId, exchangeAuth, policy, origin, beginEx);
+ server.requestType = binding.resolveRequestType(beginEx);
+ boolean headersValid = server.onDecodeHeaders(server.routedId, route.id, traceId, exchangeAuth,
+ policy, origin, beginEx);
+ if (!headersValid)
+ {
+ error = response400;
+ }
}
else
{
@@ -1567,6 +1583,7 @@ private final class HttpServer
private long replyAck;
private long replyBudgetId;
private int replyMax;
+ private HttpRequestType requestType;
private HttpServer(
HttpBindingConfig binding,
@@ -2116,6 +2133,30 @@ private void decodeNetwork(
}
}
+ private void onDecodeBodyInvalid(
+ long traceId,
+ long authorization,
+ DirectBuffer error)
+ {
+ HttpExchangeState responseState = exchange.responseState;
+ exchange.doRequestAbort(traceId, EMPTY_OCTETS);
+ exchange.doResponseReset(traceId);
+
+ if (responseState != HttpExchangeState.OPEN)
+ {
+ replyCloseOnFlush = true;
+ doNetworkData(traceId, authorization, 0L, error.capacity() + replyPad, error, 0, error.capacity());
+ if (encodeSlot == NO_SLOT)
+ {
+ doNetworkEnd(traceId, authorization);
+ }
+ }
+ else
+ {
+ doNetworkAbort(traceId, authorization);
+ }
+ }
+
private void onDecodeHeadersError(
long traceId,
long authorization,
@@ -2205,7 +2246,7 @@ private void onDecodeCorsPreflight(
assert exchange == null;
}
- private void onDecodeHeaders(
+ private boolean onDecodeHeaders(
long originId,
long routedId,
long traceId,
@@ -2214,14 +2255,19 @@ private void onDecodeHeaders(
String origin,
HttpBeginExFW beginEx)
{
- final HttpExchange exchange = new HttpExchange(originId, routedId, authorization, traceId, policy, origin);
- exchange.doRequestBegin(traceId, beginEx);
- exchange.doResponseWindow(traceId);
+ boolean headersValid = binding.validateHeaders(requestType, beginEx);
+ if (headersValid)
+ {
+ final HttpExchange exchange = new HttpExchange(originId, routedId, authorization, traceId, policy, origin);
+ exchange.doRequestBegin(traceId, beginEx);
+ exchange.doResponseWindow(traceId);
- final HttpHeaderFW connection = beginEx.headers().matchFirst(h -> HEADER_CONNECTION.equals(h.name()));
- exchange.responseClosing = connection != null && connectionClose.reset(connection.value().asString()).matches();
+ final HttpHeaderFW connection = beginEx.headers().matchFirst(h -> HEADER_CONNECTION.equals(h.name()));
+ exchange.responseClosing = connection != null && connectionClose.reset(connection.value().asString()).matches();
- this.exchange = exchange;
+ this.exchange = exchange;
+ }
+ return headersValid;
}
private void onDecodeHeadersOnly(
@@ -2244,7 +2290,18 @@ private int onDecodeBody(
int limit,
Flyweight extension)
{
- return exchange.doRequestData(traceId, budgetId, buffer, offset, limit, extension);
+ boolean contentValid = binding.validateContent(requestType, buffer, 0, limit - offset);
+ int result;
+ if (contentValid)
+ {
+ result = exchange.doRequestData(traceId, budgetId, buffer, offset, limit, extension);
+ }
+ else
+ {
+ onDecodeBodyInvalid(traceId, authorization, ERROR_400_BAD_REQUEST);
+ result = limit;
+ }
+ return result;
}
private void onDecodeTrailers(
@@ -4833,10 +4890,6 @@ else if (!isCorsRequestAllowed(binding, headers))
HttpPolicyConfig policy = binding.access().effectivePolicy(headers);
final String origin = policy == CROSS_ORIGIN ? headers.get(HEADER_NAME_ORIGIN) : null;
- final Http2Exchange exchange =
- new Http2Exchange(originId, routedId, NO_REQUEST_ID, streamId, exchangeAuth,
- traceId, policy, origin, contentLength);
-
if (binding.options != null && binding.options.overrides != null)
{
binding.options.overrides.forEach((k, v) -> headers.put(k.asString(), v.asString()));
@@ -4847,11 +4900,23 @@ else if (!isCorsRequestAllowed(binding, headers))
.headers(hs -> headers.forEach((n, v) -> hs.item(h -> h.name(n).value(v))))
.build();
- exchange.doRequestBegin(traceId, beginEx);
+ HttpRequestType requestType = binding.resolveRequestType(beginEx);
+
+ final Http2Exchange exchange = new Http2Exchange(originId, routedId, NO_REQUEST_ID, streamId,
+ exchangeAuth, traceId, policy, origin, contentLength, requestType);
- if (endRequest)
+ boolean headersValid = binding.validateHeaders(requestType, beginEx);
+ if (headersValid)
{
- exchange.doRequestEnd(traceId, EMPTY_OCTETS);
+ exchange.doRequestBegin(traceId, beginEx);
+ if (endRequest)
+ {
+ exchange.doRequestEnd(traceId, EMPTY_OCTETS);
+ }
+ }
+ else
+ {
+ doEncodeHeaders(traceId, authorization, streamId, headers400, true);
}
}
}
@@ -5054,25 +5119,40 @@ private int onDecodeData(
else
{
final int payloadLength = payload.capacity();
-
- if (payloadLength > 0)
+ boolean contentValid = binding.validateContent(exchange.request, payload, 0, payloadLength);
+ if (contentValid)
{
- payloadRemaining.set(payloadLength);
- exchange.doRequestData(traceId, payload, payloadRemaining);
- progress += payloadLength - payloadRemaining.value;
- deferred += payloadRemaining.value;
- }
+ if (payloadLength > 0)
+ {
+ payloadRemaining.set(payloadLength);
+ exchange.doRequestData(traceId, payload, payloadRemaining);
+ progress += payloadLength - payloadRemaining.value;
+ deferred += payloadRemaining.value;
+ }
- if (deferred == 0 && Http2Flags.endStream(flags))
- {
- if (exchange.requestContentLength != -1 && exchange.contentObserved != exchange.requestContentLength)
+ if (deferred == 0 && Http2Flags.endStream(flags))
{
- doEncodeRstStream(traceId, streamId, Http2ErrorCode.PROTOCOL_ERROR);
+ if (exchange.requestContentLength != -1 &&
+ exchange.contentObserved != exchange.requestContentLength)
+ {
+ doEncodeRstStream(traceId, streamId, Http2ErrorCode.PROTOCOL_ERROR);
+ }
+ else
+ {
+ exchange.doRequestEnd(traceId, EMPTY_OCTETS);
+ }
}
- else
+ }
+ else
+ {
+ if (!HttpState.replyOpened(exchange.state))
{
- exchange.doRequestEnd(traceId, EMPTY_OCTETS);
+ doEncodeHeaders(traceId, authorization, streamId, headers400, true);
}
+ doEncodeRstStream(traceId, streamId, Http2ErrorCode.CANCEL);
+ exchange.doRequestAbort(traceId, EMPTY_OCTETS);
+ exchange.doResponseReset(traceId);
+ progress += payloadLength;
}
}
}
@@ -5206,7 +5286,7 @@ private void doEncodePromise(
doEncodePushPromise(traceId, authorization, pushId, promiseId, promise);
final Http2Exchange exchange = new Http2Exchange(originId, routedId, requestId, promiseId,
- exchangeAuth, traceId, policy, origin, contentLength);
+ exchangeAuth, traceId, policy, origin, contentLength, null);
final HttpBeginExFW beginEx = beginExRW.wrap(extBuffer, 0, extBuffer.capacity())
.typeId(httpTypeId)
@@ -5522,6 +5602,8 @@ private final class Http2Exchange
private long responseAck;
private int responseMax;
+ private final HttpRequestType request;
+
private Http2Exchange(
long originId,
long routedId,
@@ -5531,7 +5613,8 @@ private Http2Exchange(
long traceId,
HttpPolicyConfig policy,
String origin,
- long requestContentLength)
+ long requestContentLength,
+ HttpRequestType request)
{
this.originId = originId;
this.routedId = routedId;
@@ -5543,6 +5626,7 @@ private Http2Exchange(
this.requestId = requestId == NO_REQUEST_ID ? supplyInitialId.applyAsLong(routedId) : requestId;
this.responseId = supplyReplyId.applyAsLong(this.requestId);
this.expiringId = expireIfNecessary(guard, sessionId, originId, routedId, replyId, traceId, streamId);
+ this.request = request;
}
private int initialWindow()
@@ -5573,6 +5657,7 @@ private void doRequestData(
MutableInteger remaining)
{
assert HttpState.initialOpening(state);
+ assert !HttpState.initialClosing(state);
if (localBudget < remaining.value)
{
@@ -5611,6 +5696,8 @@ private void doRequestEnd(
long traceId,
Flyweight extension)
{
+ assert !HttpState.initialClosing(state);
+
if (!HttpState.initialOpened(state))
{
state = HttpState.closingInitial(state);
diff --git a/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparatorTest.java b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparatorTest.java
new file mode 100644
index 0000000000..aeebd13bcd
--- /dev/null
+++ b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpQueryStringComparatorTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.config;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.comparator.ComparatorMatcherBuilder.comparedBy;
+
+import org.junit.Test;
+
+public class HttpQueryStringComparatorTest
+{
+ private final HttpQueryStringComparator comparator = new HttpQueryStringComparator();
+
+ @Test
+ public void shouldCompareSameUnencodedEqualsToUnencoded()
+ {
+ assertThat("hello", comparedBy(comparator).comparesEqualTo("hello"));
+ }
+
+ @Test
+ public void shouldCompareSameUnencodedEqualsToEncoded()
+ {
+ assertThat("hello", comparedBy(comparator).comparesEqualTo("%68%65%6C%6C%6F"));
+ }
+
+ @Test
+ public void shouldCompareSameEncodedEqualsToUnencoded()
+ {
+ assertThat("%68%65%6C%6C%6F", comparedBy(comparator).comparesEqualTo("hello"));
+ }
+
+ @Test
+ public void shouldCompareSameEncodedEqualsToEncoded()
+ {
+ assertThat("%68%65%6c%6c%6f", comparedBy(comparator).comparesEqualTo("%68%65%6C%6C%6F"));
+ }
+
+ @Test
+ public void shouldCompareSmallerUnencodedLessThanLargerUnencoded()
+ {
+ assertThat("ciao", comparedBy(comparator).lessThan("hello"));
+ }
+
+ @Test
+ public void shouldCompareSmallerUnencodedLessThanLargerEncoded()
+ {
+ assertThat("ciao", comparedBy(comparator).lessThan("%68%65%6C%6C%6F"));
+ }
+
+ @Test
+ public void shouldCompareSmallerEncodedLessThanLargerUnencoded()
+ {
+ assertThat("%63%69%61%6F", comparedBy(comparator).lessThan("hello"));
+ }
+
+ @Test
+ public void shouldCompareSmallerEncodedLessThanLargerEncoded()
+ {
+ assertThat("%63%69%61%6F", comparedBy(comparator).lessThan("%68%65%6c%6c%6f"));
+ }
+
+ @Test
+ public void shouldCompareShorterUnencodedLessThanLongerUnencoded()
+ {
+ assertThat("hello", comparedBy(comparator).lessThan("hello1"));
+ }
+
+ @Test
+ public void shouldCompareShorterUnencodedLessThanLongerEncoded()
+ {
+ assertThat("hello", comparedBy(comparator).lessThan("%68%65%6C%6C%6F%49"));
+ }
+
+ @Test
+ public void shouldCompareShorterEncodedLessThanLongerUnencoded()
+ {
+ assertThat("%68%65%6C%6C%6F", comparedBy(comparator).lessThan("hello1"));
+ }
+
+ @Test
+ public void shouldCompareShorterEncodedLessThanLongerEncoded()
+ {
+ assertThat("%68%65%6C%6C%6F", comparedBy(comparator).lessThan("%68%65%6C%6C%6F%49"));
+ }
+
+ @Test
+ public void shouldCompareLargerUnencodedGreaterThanSmallerUnencoded()
+ {
+ assertThat("hello", comparedBy(comparator).greaterThan("ciao"));
+ }
+
+ @Test
+ public void shouldCompareLargerUnencodedGreaterThanSmallerEncoded()
+ {
+ assertThat("hello", comparedBy(comparator).greaterThan("%63%69%61%6F"));
+ }
+
+ @Test
+ public void shouldCompareLargerEncodedGreaterThanSmallerUnencoded()
+ {
+ assertThat("%68%65%6C%6C%6F", comparedBy(comparator).greaterThan("ciao"));
+ }
+
+ @Test
+ public void shouldCompareLargerEncodedGreaterThanSmallerEncoded()
+ {
+ assertThat("%68%65%6C%6C%6F", comparedBy(comparator).greaterThan("%63%69%61%6F"));
+ }
+
+ @Test
+ public void shouldCompareLongerUnencodedGreaterThanShorterUnencoded()
+ {
+ assertThat("hello1", comparedBy(comparator).greaterThan("hello"));
+ }
+
+ @Test
+ public void shouldCompareLongerUnencodedGreaterThanShorterEncoded()
+ {
+ assertThat("hello1", comparedBy(comparator).greaterThan("%68%65%6C%6C%6F"));
+ }
+
+ @Test
+ public void shouldCompareLongerEncodedGreaterThanShorterUnencoded()
+ {
+ assertThat("%68%65%6C%6C%6F%49", comparedBy(comparator).greaterThan("hello"));
+ }
+
+ @Test
+ public void shouldCompareLongerEncodedGreaterThanShorterEncoded()
+ {
+ assertThat("%68%65%6C%6C%6F%49", comparedBy(comparator).greaterThan("%68%65%6C%6C%6F"));
+ }
+}
diff --git a/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestTypeTest.java b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestTypeTest.java
new file mode 100644
index 0000000000..853c3db17a
--- /dev/null
+++ b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/config/HttpRequestTypeTest.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.config;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import java.util.regex.Matcher;
+
+import org.junit.Test;
+
+public class HttpRequestTypeTest
+{
+ @Test
+ public void shouldParsePath()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathWithSlash()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathWithQuestionMark()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield?";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathWithSlashAndQuestionMark()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/?";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathAndQuery()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield?hello=ciao&day=nap&answer=42";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("hello"));
+ assertThat(queryMatcher.group(2), equalTo("ciao"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("day"));
+ assertThat(queryMatcher.group(2), equalTo("nap"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("answer"));
+ assertThat(queryMatcher.group(2), equalTo("42"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathWithSlashAndQuery()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/?answer=42";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("answer"));
+ assertThat(queryMatcher.group(2), equalTo("42"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathAndQueryWithAmpersand()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/?answer=42&";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("answer"));
+ assertThat(queryMatcher.group(2), equalTo("42"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathAndIncompleteQuery1()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/?hello=ciao&answer=";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("hello"));
+ assertThat(queryMatcher.group(2), equalTo("ciao"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+
+ @Test
+ public void shouldParsePathAndIncompleteQuery2()
+ {
+ // GIVEN
+ String configPath = "/valid/{category}/{id}";
+ String actualPath = "/valid/cat/garfield/?hello=ciao&answer";
+ HttpRequestType requestType = HttpRequestType.builder()
+ .path(configPath)
+ .build();
+
+ // WHEN
+ Matcher pathMatcher = requestType.pathMatcher.reset(actualPath);
+ Matcher queryMatcher = requestType.queryMatcher.reset(actualPath);
+
+ // THEN
+ boolean pathMatches = pathMatcher.matches();
+ assertThat(pathMatches, equalTo(true));
+ assertThat(pathMatcher.group("category"), equalTo("cat"));
+ assertThat(pathMatcher.group("id"), equalTo("garfield"));
+
+ boolean queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(true));
+ assertThat(queryMatcher.group(1), equalTo("hello"));
+ assertThat(queryMatcher.group(2), equalTo("ciao"));
+
+ queryFound = queryMatcher.find();
+ assertThat(queryFound, equalTo(false));
+ }
+}
diff --git a/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7230/server/ValidationIT.java b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7230/server/ValidationIT.java
new file mode 100644
index 0000000000..831b5e9c02
--- /dev/null
+++ b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7230/server/ValidationIT.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.streams.rfc7230.server;
+
+import static io.aklivity.zilla.runtime.binding.http.internal.HttpConfiguration.HTTP_CONCURRENT_STREAMS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+import io.aklivity.zilla.runtime.engine.test.EngineRule;
+import io.aklivity.zilla.runtime.engine.test.annotation.Configuration;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("net", "io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation")
+ .addScriptRoot("app", "io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
+
+ private final EngineRule engine = new EngineRule()
+ .directory("target/zilla-itests")
+ .countersBufferCapacity(8192)
+ .configurationRoot("io/aklivity/zilla/specs/binding/http/config/v1.1")
+ .configure(HTTP_CONCURRENT_STREAMS, 100)
+ .external("app0")
+ .clean();
+
+ @Rule
+ public final TestRule chain = outerRule(engine).around(k3po).around(timeout);
+
+ @Test
+ @Configuration("server.validation.yaml")
+ @Specification({
+ "${net}/invalid/client",
+ "${app}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Configuration("server.validation.yaml")
+ @Specification({
+ "${net}/valid/client",
+ "${app}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+}
diff --git a/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7540/server/ValidationIT.java b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7540/server/ValidationIT.java
new file mode 100644
index 0000000000..1d82bc0d13
--- /dev/null
+++ b/runtime/binding-http/src/test/java/io/aklivity/zilla/runtime/binding/http/internal/streams/rfc7540/server/ValidationIT.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.runtime.binding.http.internal.streams.rfc7540.server;
+
+import static io.aklivity.zilla.runtime.binding.http.internal.HttpConfiguration.HTTP_CONCURRENT_STREAMS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+import io.aklivity.zilla.runtime.engine.test.EngineRule;
+import io.aklivity.zilla.runtime.engine.test.annotation.Configuration;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("net", "io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation")
+ .addScriptRoot("app", "io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
+
+ private final EngineRule engine = new EngineRule()
+ .directory("target/zilla-itests")
+ .countersBufferCapacity(8192)
+ .configurationRoot("io/aklivity/zilla/specs/binding/http/config/v2")
+ .configure(HTTP_CONCURRENT_STREAMS, 100)
+ .external("app0")
+ .clean();
+
+ @Rule
+ public final TestRule chain = outerRule(engine).around(k3po).around(timeout);
+
+ @Test
+ @Configuration("server.validation.yaml")
+ @Specification({
+ "${net}/invalid/client",
+ "${app}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Configuration("server.validation.yaml")
+ @Specification({
+ "${net}/valid/client",
+ "${app}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+}
diff --git a/runtime/binding-kafka-grpc/pom.xml b/runtime/binding-kafka-grpc/pom.xml
index dd17e611a6..c9ea512e25 100644
--- a/runtime/binding-kafka-grpc/pom.xml
+++ b/runtime/binding-kafka-grpc/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-kafka/pom.xml b/runtime/binding-kafka/pom.xml
index a08acb8591..8829db813d 100644
--- a/runtime/binding-kafka/pom.xml
+++ b/runtime/binding-kafka/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaCacheClientProduceFactory.java b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaCacheClientProduceFactory.java
index 6a9f4de614..0d93e0ad1c 100644
--- a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaCacheClientProduceFactory.java
+++ b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaCacheClientProduceFactory.java
@@ -21,8 +21,10 @@
import static io.aklivity.zilla.runtime.engine.concurrent.Signaler.NO_CANCEL_ID;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
+import static java.time.Instant.now;
import static java.util.concurrent.TimeUnit.SECONDS;
+import java.nio.ByteBuffer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.LongFunction;
@@ -86,11 +88,15 @@
public final class KafkaCacheClientProduceFactory implements BindingHandler
{
private static final OctetsFW EMPTY_OCTETS = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0);
+ private static final KafkaKeyFW EMPTY_KEY =
+ new OctetsFW().wrap(new UnsafeBuffer(ByteBuffer.wrap(new byte[] { 0x00 })), 0, 1)
+ .get(new KafkaKeyFW()::wrap);
private static final Consumer EMPTY_EXTENSION = ex -> {};
private static final Array32FW EMPTY_TRAILERS =
new Array32FW.Builder<>(new KafkaHeaderFW.Builder(), new KafkaHeaderFW())
.wrap(new UnsafeBuffer(new byte[8]), 0, 8)
.build();
+ private static final int PRODUCE_FLUSH_SEQUENCE = -1;
private static final int ERROR_NOT_LEADER_FOR_PARTITION = 6;
private static final int ERROR_RECORD_LIST_TOO_LARGE = 18;
@@ -762,6 +768,47 @@ private void onClientInitialData(
creditor.credit(traceId, partitionIndex, reserved);
}
+ private void onClientInitialFlush(
+ KafkaCacheClientProduceStream stream,
+ FlushFW flush)
+ {
+ final long traceId = flush.traceId();
+ final int reserved = flush.reserved();
+
+ stream.segment = partition.newHeadIfNecessary(partitionOffset, EMPTY_KEY, 0, 0);
+
+ int error = NO_ERROR;
+ if (stream.segment != null)
+ {
+ final long nextOffset = partition.nextOffset(defaultOffset);
+ assert partitionOffset >= 0 && partitionOffset >= nextOffset
+ : String.format("%d >= 0 && %d >= %d", partitionOffset, partitionOffset, nextOffset);
+
+ final long keyHash = partition.computeKeyHash(EMPTY_KEY);
+ partition.writeProduceEntryStart(partitionOffset, stream.segment, stream.entryMark, stream.position,
+ now().toEpochMilli(), stream.initialId, PRODUCE_FLUSH_SEQUENCE,
+ KafkaAckMode.LEADER_ONLY, EMPTY_KEY, keyHash, 0, EMPTY_TRAILERS, trailersSizeMax);
+ stream.partitionOffset = partitionOffset;
+ partitionOffset++;
+
+ Array32FW trailers = EMPTY_TRAILERS;
+
+ partition.writeProduceEntryFin(stream.segment, stream.entryMark, stream.position, stream.initialSeq, trailers);
+ flushClientFanInitialIfNecessary(traceId);
+ }
+ else
+ {
+ error = ERROR_RECORD_LIST_TOO_LARGE;
+ }
+
+ if (error != NO_ERROR)
+ {
+ stream.cleanupClient(traceId, error);
+ onClientFanMemberClosed(traceId, stream);
+ }
+ creditor.credit(traceId, partitionIndex, reserved);
+ }
+
private void flushClientFanInitialIfNecessary(
long traceId)
{
@@ -1314,12 +1361,25 @@ private void onClientInitialFlush(
assert acknowledge <= sequence;
assert sequence >= initialSeq;
- initialSeq = sequence + reserved;
+ if (reserved > 0)
+ {
+ initialSeq = sequence + reserved;
- assert initialAck <= initialSeq;
+ assert initialAck <= initialSeq;
- final int noAck = (int) (initialSeq - initialAck);
- doClientInitialWindow(traceId, noAck, noAck + initialBudgetMax);
+ if (initialSeq > initialAck + initialMax)
+ {
+ doClientInitialResetIfNecessary(traceId, EMPTY_OCTETS);
+ doClientReplyAbortIfNecessary(traceId);
+ fan.onClientFanMemberClosed(traceId, this);
+ }
+ else
+ {
+ fan.onClientInitialFlush(this, flush);
+ }
+ final int noAck = (int) (initialSeq - initialAck);
+ doClientInitialWindow(traceId, noAck, initialBudgetMax);
+ }
}
private void onClientInitialEnd(
diff --git a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaMergedFactory.java b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaMergedFactory.java
index ea06bf2a43..281cb62edd 100644
--- a/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaMergedFactory.java
+++ b/runtime/binding-kafka/src/main/java/io/aklivity/zilla/runtime/binding/kafka/internal/stream/KafkaMergedFactory.java
@@ -1325,6 +1325,8 @@ private void onMergedInitialFlush(
FlushFW flush)
{
final long traceId = flush.traceId();
+ final long sequence = flush.sequence();
+ final long acknowledge = flush.acknowledge();
final OctetsFW extension = flush.extension();
final int reserved = flush.reserved();
final ExtensionFW flushEx = extension.get(extensionRO::tryWrap);
@@ -1379,6 +1381,10 @@ private void onMergedInitialFlush(
final KafkaUnmergedProduceStream producer = findProducePartitionLeader(nextPartitionId);
assert producer != null;
+
+ initialSeq = sequence + reserved;
+ assert initialAck <= initialSeq;
+
producer.doProduceInitialFlush(traceId, reserved, kafkaMergedFlushEx);
}
}
diff --git a/runtime/binding-mqtt-kafka/pom.xml b/runtime/binding-mqtt-kafka/pom.xml
index b147ed1d93..15d3d031ca 100644
--- a/runtime/binding-mqtt-kafka/pom.xml
+++ b/runtime/binding-mqtt-kafka/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-mqtt-kafka/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionFactory.java b/runtime/binding-mqtt-kafka/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionFactory.java
index 17479704a8..df13a92ef6 100644
--- a/runtime/binding-mqtt-kafka/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionFactory.java
+++ b/runtime/binding-mqtt-kafka/src/main/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionFactory.java
@@ -25,7 +25,6 @@
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.LongFunction;
import java.util.function.LongSupplier;
import java.util.function.LongUnaryOperator;
@@ -126,7 +125,6 @@ public class MqttKafkaSessionFactory implements MqttKafkaStreamFactory
private static final int SIGNAL_CONNECT_WILL_STREAM = 2;
private static final int SIGNAL_EXPIRE_SESSION = 3;
private static final int SIZE_OF_UUID = 38;
- private static final AtomicInteger CONTEXT_COUNTER = new AtomicInteger(0);
private static final int RETAIN_AVAILABLE_MASK = 1 << MqttServerCapabilities.RETAIN.value();
private static final int WILDCARD_AVAILABLE_MASK = 1 << MqttServerCapabilities.WILDCARD.value();
private static final int SUBSCRIPTION_IDS_AVAILABLE_MASK = 1 << MqttServerCapabilities.SUBSCRIPTION_IDS.value();
@@ -209,6 +207,7 @@ public class MqttKafkaSessionFactory implements MqttKafkaStreamFactory
private String serverRef;
private int reconnectAttempt;
+ private int nextContextId;
public MqttKafkaSessionFactory(
MqttKafkaConfiguration config,
@@ -960,6 +959,14 @@ private void doKafkaBegin(
long affinity)
{
reconnectAttempt = 0;
+ replySeq = 0;
+ replyAck = 0;
+ if (decodeSlot != NO_SLOT)
+ {
+ bufferPool.release(decodeSlot);
+ decodeSlot = NO_SLOT;
+ decodeSlotOffset = 0;
+ }
willFetchers.values().forEach(f -> f.cleanup(traceId, authorization));
willFetchers.clear();
@@ -1186,7 +1193,6 @@ else if (type.equals(EXPIRY_SIGNAL_NAME_OCTETS) && sessionExpiryIds.containsKey(
final MqttSessionSignalFW sessionSignal =
mqttSessionSignalRO.wrap(buffer, offset, limit);
- byte[] bytes = new byte[sessionSignal.sizeof()];
switch (sessionSignal.kind())
{
@@ -1213,7 +1219,7 @@ else if (type.equals(EXPIRY_SIGNAL_NAME_OCTETS) && sessionExpiryIds.containsKey(
case MqttSessionSignalFW.KIND_EXPIRY:
final MqttExpirySignalFW expirySignal = sessionSignal.expiry();
long expireAt = expirySignal.expireAt();
- final String16FW expiryClientId = expirySignal.clientId();
+ final String16FW expiryClientId = new String16FW(expirySignal.clientId().asString());
if (expireAt == MqttTime.UNKNOWN.value())
{
@@ -1224,7 +1230,7 @@ else if (type.equals(EXPIRY_SIGNAL_NAME_OCTETS) && sessionExpiryIds.containsKey(
expireAt = supplyTime.getAsLong() + expirySignal.delay();
}
- final int contextId = CONTEXT_COUNTER.incrementAndGet();
+ final int contextId = nextContextId++;
expiryClientIds.put(contextId, expiryClientId);
final long signalId =
diff --git a/runtime/binding-mqtt-kafka/src/test/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionProxyIT.java b/runtime/binding-mqtt-kafka/src/test/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionProxyIT.java
index 350186e2c4..272167bab1 100644
--- a/runtime/binding-mqtt-kafka/src/test/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionProxyIT.java
+++ b/runtime/binding-mqtt-kafka/src/test/java/io/aklivity/zilla/runtime/binding/mqtt/kafka/internal/stream/MqttKafkaSessionProxyIT.java
@@ -252,6 +252,15 @@ public void shouldDecodeSessionExpirySignalFragmented() throws Exception
k3po.finish();
}
+ @Test
+ @Configuration("proxy.yaml")
+ @Specification({
+ "${kafka}/session.expiry.after.signal.stream.restart/server"})
+ public void shouldExpireSessionAfterSignalStreamRestart() throws Exception
+ {
+ k3po.finish();
+ }
+
@Test
@Configuration("proxy.yaml")
@Specification({
diff --git a/runtime/binding-mqtt/pom.xml b/runtime/binding-mqtt/pom.xml
index d8df6bcde3..67e9061834 100644
--- a/runtime/binding-mqtt/pom.xml
+++ b/runtime/binding-mqtt/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-proxy/pom.xml b/runtime/binding-proxy/pom.xml
index 5b7b8e73d3..b11968e627 100644
--- a/runtime/binding-proxy/pom.xml
+++ b/runtime/binding-proxy/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-sse-kafka/pom.xml b/runtime/binding-sse-kafka/pom.xml
index b25b3e0e6d..b16730b2ff 100644
--- a/runtime/binding-sse-kafka/pom.xml
+++ b/runtime/binding-sse-kafka/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-sse/pom.xml b/runtime/binding-sse/pom.xml
index cc27443d98..c46b3153e1 100644
--- a/runtime/binding-sse/pom.xml
+++ b/runtime/binding-sse/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-tcp/pom.xml b/runtime/binding-tcp/pom.xml
index 2487f1067a..f277e2b62a 100644
--- a/runtime/binding-tcp/pom.xml
+++ b/runtime/binding-tcp/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-tls/pom.xml b/runtime/binding-tls/pom.xml
index 42a7ab6b18..d9bf79655b 100644
--- a/runtime/binding-tls/pom.xml
+++ b/runtime/binding-tls/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/binding-ws/pom.xml b/runtime/binding-ws/pom.xml
index e7bb4fa458..0a91653877 100644
--- a/runtime/binding-ws/pom.xml
+++ b/runtime/binding-ws/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/command-metrics/pom.xml b/runtime/command-metrics/pom.xml
index 9eda922a03..b8a1a5710b 100644
--- a/runtime/command-metrics/pom.xml
+++ b/runtime/command-metrics/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/command-start/pom.xml b/runtime/command-start/pom.xml
index 3480fe8ddf..65f0484b05 100644
--- a/runtime/command-start/pom.xml
+++ b/runtime/command-start/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/command-stop/pom.xml b/runtime/command-stop/pom.xml
index 47026881ea..38d0bcb9e9 100644
--- a/runtime/command-stop/pom.xml
+++ b/runtime/command-stop/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/command/pom.xml b/runtime/command/pom.xml
index 6b596bedcd..303b2025e6 100644
--- a/runtime/command/pom.xml
+++ b/runtime/command/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/engine/pom.xml b/runtime/engine/pom.xml
index d3ef4fb13c..79e0a9ca24 100644
--- a/runtime/engine/pom.xml
+++ b/runtime/engine/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/exporter-prometheus/pom.xml b/runtime/exporter-prometheus/pom.xml
index 88bef67d94..2598d59116 100644
--- a/runtime/exporter-prometheus/pom.xml
+++ b/runtime/exporter-prometheus/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/guard-jwt/pom.xml b/runtime/guard-jwt/pom.xml
index 99a443e2f6..76cae666fd 100644
--- a/runtime/guard-jwt/pom.xml
+++ b/runtime/guard-jwt/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/metrics-grpc/pom.xml b/runtime/metrics-grpc/pom.xml
index d64605b94e..d5d52d6186 100644
--- a/runtime/metrics-grpc/pom.xml
+++ b/runtime/metrics-grpc/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/metrics-http/pom.xml b/runtime/metrics-http/pom.xml
index 7557a36363..0d3ecfa335 100644
--- a/runtime/metrics-http/pom.xml
+++ b/runtime/metrics-http/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/metrics-stream/pom.xml b/runtime/metrics-stream/pom.xml
index 3294d7ac3e..a254ed5ba7 100644
--- a/runtime/metrics-stream/pom.xml
+++ b/runtime/metrics-stream/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/pom.xml b/runtime/pom.xml
index a9c4280e09..7f2f8047f8 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/runtime/vault-filesystem/pom.xml b/runtime/vault-filesystem/pom.xml
index 5b86590610..178a5362a0 100644
--- a/runtime/vault-filesystem/pom.xml
+++ b/runtime/vault-filesystem/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
runtime
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-echo.spec/pom.xml b/specs/binding-echo.spec/pom.xml
index 575da131de..4d623552d9 100644
--- a/specs/binding-echo.spec/pom.xml
+++ b/specs/binding-echo.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-fan.spec/pom.xml b/specs/binding-fan.spec/pom.xml
index 4667acdc9b..7c9434d5d1 100644
--- a/specs/binding-fan.spec/pom.xml
+++ b/specs/binding-fan.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-filesystem.spec/pom.xml b/specs/binding-filesystem.spec/pom.xml
index 0981327833..3f05c6e0f9 100644
--- a/specs/binding-filesystem.spec/pom.xml
+++ b/specs/binding-filesystem.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-grpc-kafka.spec/pom.xml b/specs/binding-grpc-kafka.spec/pom.xml
index 4f74b35e6a..1a2d91db28 100644
--- a/specs/binding-grpc-kafka.spec/pom.xml
+++ b/specs/binding-grpc-kafka.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-grpc.spec/pom.xml b/specs/binding-grpc.spec/pom.xml
index 3e524410a4..5047166c46 100644
--- a/specs/binding-grpc.spec/pom.xml
+++ b/specs/binding-grpc.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-http-filesystem.spec/pom.xml b/specs/binding-http-filesystem.spec/pom.xml
index bb77c07ccf..4dddf58c61 100644
--- a/specs/binding-http-filesystem.spec/pom.xml
+++ b/specs/binding-http-filesystem.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-http-kafka.spec/pom.xml b/specs/binding-http-kafka.spec/pom.xml
index e1ef587305..30af4d5fcf 100644
--- a/specs/binding-http-kafka.spec/pom.xml
+++ b/specs/binding-http-kafka.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-http.spec/pom.xml b/specs/binding-http.spec/pom.xml
index dbe9ddf345..89d21908f9 100644
--- a/specs/binding-http.spec/pom.xml
+++ b/specs/binding-http.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v1.1/server.validation.yaml b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v1.1/server.validation.yaml
new file mode 100644
index 0000000000..16956d2571
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v1.1/server.validation.yaml
@@ -0,0 +1,48 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+---
+name: test
+bindings:
+ net0:
+ type: http
+ kind: server
+ options:
+ requests:
+ - path: /hello
+ method: GET
+ content: test
+ - path: /valid/{category}/{id}
+ method: POST
+ content-type:
+ - text/plain
+ headers:
+ code: test
+ params:
+ path:
+ category: test
+ id: test
+ query:
+ page: test
+ content:
+ type: test
+ versions:
+ - http/1.1
+ routes:
+ - exit: app0
+ when:
+ - headers:
+ :authority: localhost:8080
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v2/server.validation.yaml b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v2/server.validation.yaml
new file mode 100644
index 0000000000..a925071f02
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/config/v2/server.validation.yaml
@@ -0,0 +1,48 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+---
+name: test
+bindings:
+ net0:
+ type: http
+ kind: server
+ options:
+ requests:
+ - path: /hello
+ method: GET
+ content: test
+ - path: /valid/{category}/{id}
+ method: POST
+ content-type:
+ - text/plain
+ headers:
+ code: test
+ params:
+ path:
+ category: test
+ id: test
+ query:
+ page: test
+ content:
+ type: test
+ versions:
+ - h2
+ routes:
+ - exit: app0
+ when:
+ - headers:
+ :authority: localhost:8080
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json
index ac880b03bf..b5fee8c511 100644
--- a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.schema.patch.json
@@ -237,6 +237,114 @@
{
"type": "string"
}
+ },
+ "requests":
+ {
+ "type": "array",
+ "items":
+ {
+ "type": "object",
+ "properties":
+ {
+ "path":
+ {
+ "type": "string"
+ },
+ "method":
+ {
+ "type": "string",
+ "enum":
+ [
+ "GET",
+ "PUT",
+ "POST",
+ "DELETE",
+ "OPTIONS",
+ "HEAD",
+ "PATCH",
+ "TRACE"
+ ]
+ },
+ "content-type":
+ {
+ "type": "array",
+ "items":
+ {
+ "type": "string"
+ }
+ },
+ "headers":
+ {
+ "type": "object",
+ "patternProperties":
+ {
+ "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
+ {
+ "$ref": "#/$defs/validator/type"
+ }
+ }
+ },
+ "params":
+ {
+ "type": "object",
+ "properties":
+ {
+ "path":
+ {
+ "type": "object",
+ "patternProperties":
+ {
+ "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
+ {
+ "$ref": "#/$defs/validator/type"
+ }
+ }
+ },
+ "query":
+ {
+ "type": "object",
+ "patternProperties":
+ {
+ "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
+ {
+ "$ref": "#/$defs/validator/type"
+ }
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "content":
+ {
+ "$ref": "#/$defs/validator/type"
+ }
+ },
+ "anyOf":
+ [
+ {
+ "required":
+ [
+ "path",
+ "headers"
+ ]
+ },
+ {
+ "required":
+ [
+ "path",
+ "params"
+ ]
+ },
+ {
+ "required":
+ [
+ "path",
+ "content"
+ ]
+ }
+ ],
+ "additionalProperties": false
+ }
}
},
"additionalProperties": false
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.with.validators.schema.patch.json b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.with.validators.schema.patch.json
deleted file mode 100644
index b5fee8c511..0000000000
--- a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/schema/http.with.validators.schema.patch.json
+++ /dev/null
@@ -1,400 +0,0 @@
-[
- {
- "op": "add",
- "path": "/$defs/binding/properties/type/enum/-",
- "value": "http"
- },
- {
- "op": "add",
- "path": "/$defs/binding/allOf/-",
- "value":
- {
- "if":
- {
- "properties":
- {
- "type":
- {
- "const": "http"
- }
- }
- },
- "then":
- {
- "properties":
- {
- "type":
- {
- "const": "http"
- },
- "kind":
- {
- "enum": [ "server", "client" ]
- },
- "vault": false,
- "options":
- {
- "properties":
- {
- "versions":
- {
- "title": "Versions",
- "type": "array",
- "default": [ "http/1.1", "h2" ],
- "items":
- {
- "title": "Version",
- "type": "string",
- "enum": [ "http/1.1", "h2" ]
- }
- },
- "access-control":
- {
- "title": "Access Control",
- "type": "object",
- "properties":
- {
- "policy":
- {
- "title": "Policy",
- "type": "string"
- }
- },
- "oneOf":
- [
- {
- "properties":
- {
- "policy":
- {
- "const": "same-origin"
- }
- }
- },
- {
- "properties":
- {
- "policy":
- {
- "const": "cross-origin"
- },
- "allow":
- {
- "title": "Allow",
- "type": "object",
- "properties":
- {
- "origins":
- {
- "title": "Origins",
- "type": "array",
- "items":
- {
- "type": "string"
- }
- },
- "methods":
- {
- "title": "Methods",
- "type": "array",
- "items":
- {
- "type": "string"
- }
- },
- "headers":
- {
- "title": "Headers",
- "type": "array",
- "items":
- {
- "type": "string"
- }
- },
- "credentials":
- {
- "title": "Credentials",
- "type": "boolean"
- }
- },
- "additionalProperties": false
- },
- "max-age":
- {
- "title": "Max Age",
- "type": "number"
- },
- "expose":
- {
- "title": "Expose",
- "type": "object",
- "properties":
- {
- "headers":
- {
- "title": "Headers",
- "type": "array",
- "items":
- {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- }
- },
- "additionalProperties": false
- }
- ]
- },
- "authorization":
- {
- "title": "Authorizations",
- "type": "object",
- "patternProperties":
- {
- "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
- {
- "title": "Authorization",
- "type": "object",
- "properties":
- {
- "credentials":
- {
- "title": "Credentials",
- "type": "object",
- "properties":
- {
- "cookies":
- {
- "title": "Cookies",
- "type": "object",
- "additionalProperties":
- {
- "type": "string",
- "pattern": ".*\\{credentials\\}.*"
- }
- },
- "headers":
- {
- "title": "Headers",
- "type": "object",
- "additionalProperties":
- {
- "type": "string",
- "pattern": ".*\\{credentials\\}.*"
- }
- },
- "query":
- {
- "title": "Query Parameters",
- "type": "object",
- "additionalProperties":
- {
- "type": "string",
- "pattern": ".*\\{credentials\\}.*"
- }
- }
- },
- "additionalProperties": false,
- "anyOf":
- [
- {
- "required":
- [
- "cookies"
- ]
- },
- {
- "required":
- [
- "headers"
- ]
- },
- {
- "required":
- [
- "query"
- ]
- }
- ]
- }
- },
- "additionalProperties": false,
- "required":
- [
- "credentials"
- ]
- }
- },
- "maxProperties": 1
- },
- "overrides":
- {
- "title": "Overrides",
- "type": "object",
- "additionalProperties" :
- {
- "type": "string"
- }
- },
- "requests":
- {
- "type": "array",
- "items":
- {
- "type": "object",
- "properties":
- {
- "path":
- {
- "type": "string"
- },
- "method":
- {
- "type": "string",
- "enum":
- [
- "GET",
- "PUT",
- "POST",
- "DELETE",
- "OPTIONS",
- "HEAD",
- "PATCH",
- "TRACE"
- ]
- },
- "content-type":
- {
- "type": "array",
- "items":
- {
- "type": "string"
- }
- },
- "headers":
- {
- "type": "object",
- "patternProperties":
- {
- "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
- {
- "$ref": "#/$defs/validator/type"
- }
- }
- },
- "params":
- {
- "type": "object",
- "properties":
- {
- "path":
- {
- "type": "object",
- "patternProperties":
- {
- "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
- {
- "$ref": "#/$defs/validator/type"
- }
- }
- },
- "query":
- {
- "type": "object",
- "patternProperties":
- {
- "^[a-zA-Z]+[a-zA-Z0-9\\._\\-]*$":
- {
- "$ref": "#/$defs/validator/type"
- }
- }
- }
- },
- "additionalProperties": false
- },
- "content":
- {
- "$ref": "#/$defs/validator/type"
- }
- },
- "anyOf":
- [
- {
- "required":
- [
- "path",
- "headers"
- ]
- },
- {
- "required":
- [
- "path",
- "params"
- ]
- },
- {
- "required":
- [
- "path",
- "content"
- ]
- }
- ],
- "additionalProperties": false
- }
- }
- },
- "additionalProperties": false
- },
- "routes":
- {
- "items":
- {
- "properties":
- {
- "when":
- {
- "items":
- {
- "properties":
- {
- "headers":
- {
- "title": "Headers",
- "type": "object",
- "additionalProperties" :
- {
- "type": "string"
- }
- }
- },
- "additionalProperties": false
- }
- },
- "with": false
- }
- }
- }
- },
- "anyOf":
- [
- {
- "required":
- [
- "exit"
- ]
- },
- {
- "required":
- [
- "routes"
- ]
- }
- ]
- }
- }
- }
-]
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/client.rpt
new file mode 100644
index 0000000000..872011685e
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/client.rpt
@@ -0,0 +1,38 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+# Request 4 - invalid content
+
+# We receive a begin frame as the headers are valid
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain")
+ .header("content-length", "3")
+ .build()}
+connected
+
+# We receive an abort frame as the content is invalid
+write abort
+read abort
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/server.rpt
new file mode 100644
index 0000000000..bdb39ddab1
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/invalid/server.rpt
@@ -0,0 +1,39 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+accept "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+accepted
+
+# Request 4 - invalid content
+
+# We receive a begin frame as the headers are valid
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain")
+ .header("content-length", "3")
+ .build()}
+connected
+
+# We receive an abort frame as the content is invalid
+read aborted
+write aborted
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/client.rpt
new file mode 100644
index 0000000000..636f5ed0d5
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/client.rpt
@@ -0,0 +1,155 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+# Request 1 - valid path params
+connect "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response1"
+read closed
+read notify RESPONSE_ONE_RECEIVED
+
+
+# Request 2 - valid path params, query param
+connect await RESPONSE_ONE_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response2"
+read closed
+read notify RESPONSE_TWO_RECEIVED
+
+
+# Request 3 - valid path params, url encoded query param
+connect await RESPONSE_TWO_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response3"
+read closed
+read notify RESPONSE_THREE_RECEIVED
+
+
+# Request 4 - valid path params, query param, header field
+connect await RESPONSE_THREE_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response4"
+read closed
+read notify RESPONSE_FOUR_RECEIVED
+
+
+# Request 5 - valid path params, query param, header field, valid content
+connect await RESPONSE_FOUR_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+
+write "1234567890123"
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response5"
+read closed
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/server.rpt
new file mode 100644
index 0000000000..5cd1eaf09f
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation/valid/server.rpt
@@ -0,0 +1,137 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+accept "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+# Request 1 - valid path params
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response1"
+write close
+
+
+# Request 2 - valid path params, query param
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response2"
+write close
+
+
+# Request 3 - valid path params, url encoded query param
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response3"
+write close
+
+
+# Request 4 - valid path params, query param, header field
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response4"
+write close
+
+
+# Request 5 - valid path params, query param, header field, valid content
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+
+read "1234567890123"
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response5"
+write close
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/client.rpt
new file mode 100644
index 0000000000..872011685e
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/client.rpt
@@ -0,0 +1,38 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+# Request 4 - invalid content
+
+# We receive a begin frame as the headers are valid
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain")
+ .header("content-length", "3")
+ .build()}
+connected
+
+# We receive an abort frame as the content is invalid
+write abort
+read abort
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/server.rpt
new file mode 100644
index 0000000000..bdb39ddab1
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/invalid/server.rpt
@@ -0,0 +1,39 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+accept "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+accepted
+
+# Request 4 - invalid content
+
+# We receive a begin frame as the headers are valid
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain")
+ .header("content-length", "3")
+ .build()}
+connected
+
+# We receive an abort frame as the content is invalid
+read aborted
+write aborted
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/client.rpt
new file mode 100644
index 0000000000..28ca1409d8
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/client.rpt
@@ -0,0 +1,159 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+# Request 1 - valid path params
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response1"
+read closed
+read notify RESPONSE_ONE_RECEIVED
+
+
+# Request 2 - valid path params, query param
+connect await RESPONSE_ONE_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response2"
+read closed
+read notify RESPONSE_TWO_RECEIVED
+
+
+# Request 3 - valid path params, url encoded query param
+connect await RESPONSE_TWO_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response3"
+read closed
+read notify RESPONSE_THREE_RECEIVED
+
+
+# Request 4 - valid path params, query param, header field
+connect await RESPONSE_THREE_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response4"
+read closed
+
+read notify RESPONSE_FOUR_RECEIVED
+
+
+# Request 5 - valid path params, query param, header field, valid content
+connect await RESPONSE_FOUR_RECEIVED
+ "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain;charset=UTF-8")
+ .header("content-length", "13")
+ .build()}
+connected
+
+write "1234567890123"
+write close
+
+read zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+
+read "response5"
+read closed
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/server.rpt
new file mode 100644
index 0000000000..9ea96bbaf3
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation/valid/server.rpt
@@ -0,0 +1,139 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+accept "zilla://streams/app0"
+ option zilla:window 8192
+ option zilla:transmission "half-duplex"
+
+# Request 1 - valid path params
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response1"
+write close
+
+
+# Request 2 - valid path params, query param
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response2"
+write close
+
+
+# Request 3 - valid path params, url encoded query param
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123")
+ .header(":authority", "localhost:8080")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response3"
+write close
+
+
+# Request 4 - valid path params, query param, header field
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .build()}
+connected
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response4"
+write close
+
+
+# Request 5 - valid path params, query param, header field, content
+accepted
+read zilla:begin.ext ${http:matchBeginEx()
+ .typeId(zilla:id("http"))
+ .header(":scheme", "http")
+ .header(":method", "POST")
+ .header(":path", "/valid/1234567890123/1234567890123?page=1234567890123")
+ .header(":authority", "localhost:8080")
+ .header("code", "1234567890123")
+ .header("content-type", "text/plain;charset=UTF-8")
+ .header("content-length", "13")
+ .build()}
+connected
+
+read "1234567890123"
+read closed
+
+write zilla:begin.ext ${http:beginEx()
+ .typeId(zilla:id("http"))
+ .header(":status", "200")
+ .header("content-length", "9")
+ .build()}
+write flush
+
+write "response5"
+write close
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/client.rpt
new file mode 100644
index 0000000000..dfa1b825e6
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/client.rpt
@@ -0,0 +1,80 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# Request 1 - invalid path param
+write "POST /valid/1234567890123/123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 400 Bad Request\r\n"
+read "Connection: close\r\n"
+read "\r\n"
+read closed
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# Request 2 - invalid query param
+write "POST /valid/1234567890123/1234567890123?page=123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 400 Bad Request\r\n"
+read "Connection: close\r\n"
+read "\r\n"
+read closed
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# Request 3 - invalid header field
+write "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "Code: 123" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 400 Bad Request\r\n"
+read "Connection: close\r\n"
+read "\r\n"
+read closed
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# Request 4 - invalid content
+write "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "Code: 1234567890123" "\r\n"
+write "Content-Length: 3" "\r\n"
+write "Content-Type: text/plain" "\r\n"
+write "\r\n"
+write "123"
+
+read "HTTP/1.1 400 Bad Request\r\n"
+read "Connection: close\r\n"
+read "\r\n"
+read closed
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/server.rpt
new file mode 100644
index 0000000000..6cffc94f85
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/invalid/server.rpt
@@ -0,0 +1,77 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+property serverInitialWindow 8192
+
+accept "zilla://streams/net0"
+ option zilla:window ${serverInitialWindow}
+ option zilla:transmission "duplex"
+accepted
+connected
+
+# Request 1 - invalid path param
+read "POST /valid/1234567890123/123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 400 Bad Request\r\n"
+write "Connection: close\r\n"
+write "\r\n"
+write close
+
+accepted
+connected
+
+# Request 2 - invalid query param
+read "POST /valid/1234567890123/1234567890123?page=123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 400 Bad Request\r\n"
+write "Connection: close\r\n"
+write "\r\n"
+write close
+
+accepted
+connected
+
+# Request 3 - invalid header field
+read "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "Code: 123" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 400 Bad Request\r\n"
+write "Connection: close\r\n"
+write "\r\n"
+write close
+
+accepted
+connected
+
+# Request 4 - invalid content
+read "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "Code: 1234567890123" "\r\n"
+read "Content-Length: 3" "\r\n"
+read "Content-Type: text/plain" "\r\n"
+read "\r\n"
+read "123"
+
+write "HTTP/1.1 400 Bad Request\r\n"
+write "Connection: close\r\n"
+write "\r\n"
+write close
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/client.rpt
new file mode 100644
index 0000000000..e80034ddd7
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/client.rpt
@@ -0,0 +1,79 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# Request 1 - valid path params
+write "POST /valid/1234567890123/1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 200 OK\r\n"
+read "Content-Length: 9" "\r\n"
+read "\r\n"
+read "response1"
+
+
+# Request 2 - valid path params, query param
+write "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 200 OK\r\n"
+read "Content-Length: 9" "\r\n"
+read "\r\n"
+read "response2"
+
+
+# Request 3 - valid path params, url encoded query param
+write "POST /valid/1234567890123/1234567890123?%70%61%67%65=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 200 OK\r\n"
+read "Content-Length: 9" "\r\n"
+read "\r\n"
+read "response3"
+
+
+# Request 4 - valid path params, query param, header field
+write "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "Code: 1234567890123" "\r\n"
+write "\r\n"
+
+read "HTTP/1.1 200 OK\r\n"
+read "Content-Length: 9" "\r\n"
+read "\r\n"
+read "response4"
+
+
+# Request 5 - valid path params, query param, header field, valid content
+write "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+write "Host: localhost:8080" "\r\n"
+write "Code: 1234567890123" "\r\n"
+write "Content-Length: 13" "\r\n"
+write "Content-Type: text/plain" "\r\n"
+write "\r\n"
+write "1234567890123"
+
+read "HTTP/1.1 200 OK\r\n"
+read "Content-Length: 9" "\r\n"
+read "\r\n"
+read "response5"
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/server.rpt
new file mode 100644
index 0000000000..f81ba768db
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation/valid/server.rpt
@@ -0,0 +1,82 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+property serverInitialWindow 8192
+
+accept "zilla://streams/net0"
+ option zilla:window ${serverInitialWindow}
+ option zilla:transmission "duplex"
+accepted
+connected
+
+# Request 1 - valid path params
+read "POST /valid/1234567890123/1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 200 OK\r\n"
+write "Content-Length: 9" "\r\n"
+write "\r\n"
+write "response1"
+
+
+# Request 2 - valid path params, query param
+read "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 200 OK\r\n"
+write "Content-Length: 9" "\r\n"
+write "\r\n"
+write "response2"
+
+
+# Request 3 - valid path params, url encoded query param
+read "POST /valid/1234567890123/1234567890123?%70%61%67%65=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 200 OK\r\n"
+write "Content-Length: 9" "\r\n"
+write "\r\n"
+write "response3"
+
+
+# Request 4 - valid path params, query param, header field
+read "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "Code: 1234567890123" "\r\n"
+read "\r\n"
+
+write "HTTP/1.1 200 OK\r\n"
+write "Content-Length: 9" "\r\n"
+write "\r\n"
+write "response4"
+
+
+# Request 5 - valid path params, query param, header field, valid content
+read "POST /valid/1234567890123/1234567890123?page=1234567890123 HTTP/1.1" "\r\n"
+read "Host: localhost:8080" "\r\n"
+read "Code: 1234567890123" "\r\n"
+read "Content-Length: 13" "\r\n"
+read "Content-Type: text/plain" "\r\n"
+read "\r\n"
+read "1234567890123"
+
+write "HTTP/1.1 200 OK\r\n"
+write "Content-Length: 9" "\r\n"
+write "\r\n"
+write "response5"
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/client.rpt
new file mode 100644
index 0000000000..036747bc4f
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/client.rpt
@@ -0,0 +1,142 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# client connection preface
+write "PRI * HTTP/2.0\r\n"
+ "\r\n"
+ "SM\r\n"
+ "\r\n"
+write flush
+
+# server connection preface - SETTINGS frame
+read [0x00 0x00 0x12] # length = 18
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0x00 0x00] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 0
+ [0x00 0x06 0x00 0x00 0x20 0x00] # SETTINGS_MAX_HEADER_LIST_SIZE(0x06) = 8192
+
+write [0x00 0x00 0x0c] # length = 12
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0xff 0xff] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 65535
+write flush
+
+
+# Request 1 - invalid path param
+write [0x00 0x00 0x2c] # length = 44
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x18] # :path: /valid/1234567890123/123
+ "/valid/1234567890123/123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+write flush
+
+read [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 2 - invalid query param
+write [0x00 0x00 0x3f] # length = 63
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x2B] # :path: /valid/1234567890123/1234567890123?page=123
+ "/valid/1234567890123/1234567890123?page=123"
+ [0x01] [0x0E] "localhost:8080" # :authority: localhost:8080
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 3 - invalid header field
+write [0x00 0x00 0x53] # length = 83
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x03] "123" # code: 123
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 4 - invalid content
+write [0x00 0x00 0x6e] # length = 110
+ [0x01] # HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+ [0x0f 0x10] [0x0a] "text/plain" # content-type
+ [0x0f 0x0d] [0x01] "3" # content-length
+write flush
+
+write [0x00 0x00 0x03] # length = 3
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ "123"
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/server.rpt
new file mode 100644
index 0000000000..e2ffdb4764
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/invalid/server.rpt
@@ -0,0 +1,138 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+property serverInitialWindow 8192
+
+accept "zilla://streams/net0"
+ option zilla:window ${serverInitialWindow}
+ option zilla:transmission "duplex"
+accepted
+connected
+
+# client connection preface
+read "PRI * HTTP/2.0\r\n"
+ "\r\n"
+ "SM\r\n"
+ "\r\n"
+
+# server connection preface - SETTINGS frame
+write [0x00 0x00 0x12] # length = 18
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0x00 0x00] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 0
+ [0x00 0x06 0x00 0x00 0x20 0x00] # SETTINGS_MAX_HEADER_LIST_SIZE(0x06) = 8192
+
+read [0x00 0x00 0x0c] # length = 12
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0xff 0xff] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 65535
+
+
+# Request 1 - invalid path param
+read [0x00 0x00 0x2c] # length = 44
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x18] # :path: /valid/1234567890123/123
+ "/valid/1234567890123/123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+
+write [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 2 - invalid query param
+read [0x00 0x00 0x3f] # length = 63
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x2b] # :path: /valid/1234567890123/1234567890123?page=123
+ "/valid/1234567890123/1234567890123?page=123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 3 - invalid header field
+read [0x00 0x00 0x53] # length = 83
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x03] "123" # code: 123
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
+
+
+# Request 4 - invalid content
+read [0x00 0x00 0x6e] # length = 110
+ [0x01] # HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+ [0x0f 0x10] [0x0a] "text/plain" # content-type
+ [0x0f 0x0d] [0x01] "3" # content-length
+
+read [0x00 0x00 0x03] # length = 3
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ "123"
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x8c] # :status: 400
+ [0x0f 0x0d] [0x01] "0" # content-length
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/client.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/client.rpt
new file mode 100644
index 0000000000..a06371c293
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/client.rpt
@@ -0,0 +1,211 @@
+##
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+connect "zilla://streams/net0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+connected
+
+# client connection preface
+write "PRI * HTTP/2.0\r\n"
+ "\r\n"
+ "SM\r\n"
+ "\r\n"
+write flush
+
+# server connection preface - SETTINGS frame
+read [0x00 0x00 0x12] # length = 18
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0x00 0x00] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 0
+ [0x00 0x06 0x00 0x00 0x20 0x00] # SETTINGS_MAX_HEADER_LIST_SIZE(0x06) = 8192
+
+write [0x00 0x00 0x0c] # length = 12
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0xff 0xff] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 65535
+write flush
+
+
+# Request 1 - valid path params
+write [0x00 0x00 0x36] # length = 54
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x22] # :path: /valid/1234567890123/1234567890123
+ "/valid/1234567890123/1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+write flush
+
+write [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+write flush
+
+read [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+read [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ "response1"
+
+
+# Request 2 - valid path params, query param
+write [0x00 0x00 0x49] # length = 73
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+read [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ "response2"
+
+
+# Request 3 - valid path params, url encoded query param
+write [0x00 0x00 0x51] # length = 81
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x3d] # :path: /valid/1234567890123/1234567890123?%70%61%67%65=1234567890123
+ "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+read [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ "response3"
+
+
+# Request 4 - valid path params, query param, header field
+write [0x00 0x00 0x5d] # length = 93
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+read [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ "response4"
+
+
+# Request 5 - valid path params, query param, header field, content
+write [0x00 0x00 0x7d] # length = 125
+ [0x01] # HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+ [0x0f] [0x10] # content-type
+ [0x18] "text/plain;charset=UTF-8"
+ [0x0f 0x0d] [0x02] "13" # content-length
+write flush
+
+read [0x00 0x00 0x04] # length
+ [0x08] # WINDOW_UPDATE frame
+ [0x00] # no flags
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x00 0x20 0x00] # window size increment = 8192
+
+read [0x00 0x00 0x04] # length
+ [0x08] # WINDOW_UPDATE frame
+ [0x00] # no flags
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x00 0x00 0x20 0x00] # window size increment = 8192
+
+write [0x00 0x00 0x0d] # length = 13
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ "1234567890123"
+write flush
+
+read [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+read [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ "response5"
diff --git a/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/server.rpt b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/server.rpt
new file mode 100644
index 0000000000..93180d96ed
--- /dev/null
+++ b/specs/binding-http.spec/src/main/scripts/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation/valid/server.rpt
@@ -0,0 +1,205 @@
+#
+# Copyright 2021-2023 Aklivity Inc.
+#
+# Aklivity licenses this file to you 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.
+#
+
+property serverInitialWindow 8192
+
+accept "zilla://streams/net0"
+ option zilla:window ${serverInitialWindow}
+ option zilla:transmission "duplex"
+accepted
+connected
+
+# client connection preface
+read "PRI * HTTP/2.0\r\n"
+ "\r\n"
+ "SM\r\n"
+ "\r\n"
+
+# server connection preface - SETTINGS frame
+write [0x00 0x00 0x12] # length = 18
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0x00 0x00] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 0
+ [0x00 0x06 0x00 0x00 0x20 0x00] # SETTINGS_MAX_HEADER_LIST_SIZE(0x06) = 8192
+
+read [0x00 0x00 0x0c] # length = 12
+ [0x04] # HTTP2 SETTINGS frame
+ [0x00] # flags = 0x00
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x03 0x00 0x00 0x00 0x64] # SETTINGS_MAX_CONCURRENT_STREAMS(0x03) = 100
+ [0x00 0x04 0x00 0x00 0xff 0xff] # SETTINGS_INITIAL_WINDOW_SIZE(0x04) = 65535
+
+
+# Request 1 - valid path params
+read [0x00 0x00 0x36] # length = 54
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x22] # :path: /valid/1234567890123/1234567890123
+ "/valid/1234567890123/1234567890123"
+ [0x01] [0x0E] "localhost:8080" # :authority: localhost:8080
+
+read [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+
+write [0x00 0x00 0x00] # length = 0
+ [0x04] # HTTP2 SETTINGS frame
+ [0x01] # ACK
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+write [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x01] # stream_id = 1
+ "response1"
+
+
+# Request 2 - valid path params, query param
+read [0x00 0x00 0x49] # length = 73
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+write [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x03] # stream_id = 3
+ "response2"
+
+
+# Request 3 - valid path params, url encoded query param
+read [0x00 0x00 0x51] # length = 81
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x3d] # :path: /valid/1234567890123/1234567890123?%70%61%67%65=1234567890123
+ "/valid/1234567890123/1234567890123?%70%61%67%65=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+write [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x05] # stream_id = 5
+ "response3"
+
+
+# Request 4 - valid path params, query param, header field
+read [0x00 0x00 0x5d] # length = 93
+ [0x01] # HEADERS frame
+ [0x05] # END_HEADERS | END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+write [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x07] # stream_id = 7
+ "response4"
+
+
+# Request 5 - valid path params, query param, header field, content
+read [0x00 0x00 0x7d] # length = 125
+ [0x01] # HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x83] # :method: POST
+ [0x86] # :scheme: http
+ [0x04] [0x35] # :path: /valid/1234567890123/1234567890123?page=1234567890123
+ "/valid/1234567890123/1234567890123?page=1234567890123"
+ [0x01] [0x0e] "localhost:8080" # :authority: localhost:8080
+ [0x40] [0x04] "code"
+ [0x0d] "1234567890123" # code: 1234567890123
+ [0x0f] [0x10] # content-type
+ [0x18] "text/plain;charset=UTF-8"
+ [0x0f 0x0d] [0x02] "13" # content-length
+
+write [0x00 0x00 0x04] # length
+ [0x08] # WINDOW_UPDATE frame
+ [0x00] # no flags
+ [0x00 0x00 0x00 0x00] # stream_id = 0
+ [0x00 0x00 0x20 0x00] # window size increment = 8192
+
+write [0x00 0x00 0x04] # length
+ [0x08] # WINDOW_UPDATE frame
+ [0x00] # no flags
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x00 0x00 0x20 0x00] # window size increment = 8192
+
+read [0x00 0x00 0x0d] # length = 13
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ "1234567890123"
+
+write [0x00 0x00 0x05] # length = 5
+ [0x01] # HTTP2 HEADERS frame
+ [0x04] # END_HEADERS
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ [0x88] # :status: 200
+ [0x0f 0x0d] [0x01] "9" # content-length
+
+write [0x00 0x00 0x09] # length = 9
+ [0x00] # HTTP2 DATA frame
+ [0x01] # END_STREAM
+ [0x00 0x00 0x00 0x09] # stream_id = 9
+ "response5"
diff --git a/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/ValidationIT.java b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/ValidationIT.java
new file mode 100644
index 0000000000..36887d6b3c
--- /dev/null
+++ b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/ValidationIT.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.specs.binding.http.streams.application.rfc7230;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("app", "io/aklivity/zilla/specs/binding/http/streams/application/rfc7230/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(5, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${app}/invalid/client",
+ "${app}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/valid/client",
+ "${app}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+}
diff --git a/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/ValidationIT.java b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/ValidationIT.java
new file mode 100644
index 0000000000..55854f56bf
--- /dev/null
+++ b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/ValidationIT.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.specs.binding.http.streams.application.rfc7540;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("app", "io/aklivity/zilla/specs/binding/http/streams/application/rfc7540/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(5, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${app}/invalid/client",
+ "${app}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${app}/valid/client",
+ "${app}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.finish();
+ }
+}
diff --git a/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/ValidationIT.java b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/ValidationIT.java
new file mode 100644
index 0000000000..e447c10397
--- /dev/null
+++ b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/ValidationIT.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.specs.binding.http.streams.network.rfc7230;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("net", "io/aklivity/zilla/specs/binding/http/streams/network/rfc7230/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${net}/valid/client",
+ "${net}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.start();
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${net}/invalid/client",
+ "${net}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.start();
+ k3po.finish();
+ }
+}
diff --git a/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/ValidationIT.java b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/ValidationIT.java
new file mode 100644
index 0000000000..bdb6cfb884
--- /dev/null
+++ b/specs/binding-http.spec/src/test/java/io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/ValidationIT.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021-2023 Aklivity Inc.
+ *
+ * Aklivity licenses this file to you 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 io.aklivity.zilla.specs.binding.http.streams.network.rfc7540;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.rules.RuleChain.outerRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.kaazing.k3po.junit.annotation.Specification;
+import org.kaazing.k3po.junit.rules.K3poRule;
+
+public class ValidationIT
+{
+ private final K3poRule k3po = new K3poRule()
+ .addScriptRoot("net", "io/aklivity/zilla/specs/binding/http/streams/network/rfc7540/validation");
+
+ private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS));
+
+ @Rule
+ public final TestRule chain = outerRule(k3po).around(timeout);
+
+ @Test
+ @Specification({
+ "${net}/valid/client",
+ "${net}/valid/server" })
+ public void shouldProcessValidRequests() throws Exception
+ {
+ k3po.start();
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${net}/invalid/client",
+ "${net}/invalid/server" })
+ public void shouldRejectInvalidRequests() throws Exception
+ {
+ k3po.start();
+ k3po.finish();
+ }
+}
diff --git a/specs/binding-kafka-grpc.spec/pom.xml b/specs/binding-kafka-grpc.spec/pom.xml
index 42fb9649f5..e5e2967f90 100644
--- a/specs/binding-kafka-grpc.spec/pom.xml
+++ b/specs/binding-kafka-grpc.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-kafka.spec/pom.xml b/specs/binding-kafka.spec/pom.xml
index 31f04728b9..6de19afc2a 100644
--- a/specs/binding-kafka.spec/pom.xml
+++ b/specs/binding-kafka.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/kafka/streams/application/merged/merged.produce.flush.dynamic.hashed/client.rpt b/specs/binding-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/kafka/streams/application/merged/merged.produce.flush.dynamic.hashed/client.rpt
index ffb8003d11..f4952fda14 100644
--- a/specs/binding-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/kafka/streams/application/merged/merged.produce.flush.dynamic.hashed/client.rpt
+++ b/specs/binding-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/kafka/streams/application/merged/merged.produce.flush.dynamic.hashed/client.rpt
@@ -112,8 +112,8 @@ write advise zilla:flush ${kafka:flushEx()
.merged()
.fetch()
.partition(-1, -1)
- .key("key9")
.capabilities("PRODUCE_ONLY")
+ .key("key9")
.build()
.build()}
diff --git a/specs/binding-mqtt-kafka.spec/pom.xml b/specs/binding-mqtt-kafka.spec/pom.xml
index 7e2088bd1c..96e7a01b51 100644
--- a/specs/binding-mqtt-kafka.spec/pom.xml
+++ b/specs/binding-mqtt-kafka.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/client.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/client.rpt
index 4c092b32a4..f4f989e364 100644
--- a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/client.rpt
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/client.rpt
@@ -84,6 +84,23 @@ read ${mqtt:sessionSignal()
read notify RECEIVED_WILL_DELIVER_AT_SIGNAL
+read zilla:data.ext ${kafka:matchDataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+read ${mqtt:sessionSignal()
+ .expiry()
+ .instanceId("zilla-1")
+ .clientId("client-2")
+ .delay(1000)
+ .expireAt(2000)
+ .build()
+ .build()}
write zilla:data.ext ${kafka:dataEx()
.typeId(zilla:id("kafka"))
@@ -96,6 +113,17 @@ write zilla:data.ext ${kafka:dataEx()
.build()}
write flush
+write zilla:data.ext ${kafka:dataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-2")
+ .hashKey("client-2")
+ .build()
+ .build()}
+write flush
+
connect "zilla://streams/kafka0"
option zilla:window 8192
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/server.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/server.rpt
index 861a9a642b..6cf1a76540 100644
--- a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/server.rpt
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.close.expire.session.state/server.rpt
@@ -92,6 +92,25 @@ write ${mqtt:sessionSignal()
.build()}
write flush
+write zilla:data.ext ${kafka:dataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+write ${mqtt:sessionSignal()
+ .expiry()
+ .instanceId("zilla-1")
+ .clientId("client-2")
+ .delay(1000)
+ .expireAt(2000)
+ .build()
+ .build()}
+write flush
+
# cleanup session state
read zilla:data.ext ${kafka:matchDataEx()
.typeId(zilla:id("kafka"))
@@ -104,6 +123,17 @@ read zilla:data.ext ${kafka:matchDataEx()
.build()}
read zilla:data.null
+# cleanup session state
+read zilla:data.ext ${kafka:matchDataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-2")
+ .hashKey("client-2")
+ .build()
+ .build()}
+read zilla:data.null
accepted
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/client.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/client.rpt
new file mode 100644
index 0000000000..c2c1e3be82
--- /dev/null
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/client.rpt
@@ -0,0 +1,102 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+connect "zilla://streams/kafka0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+
+write zilla:begin.ext ${kafka:beginEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .capabilities("PRODUCE_AND_FETCH")
+ .topic("mqtt-sessions")
+ .groupId("mqtt-clients")
+ .filter()
+ .header("type", "will-signal")
+ .build()
+ .filter()
+ .header("type", "expiry-signal")
+ .build()
+ .build()
+ .build()}
+
+connected
+
+read zilla:data.ext ${kafka:matchDataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+read [0x01 0x07 0x00 0x7a 0x69 0x6c 0x6c 0x61 0x2d 0x31 0x08 0x00 0x63 0x6c 0x69 0x65 0x6e 0x74 0x2d]
+
+read aborted
+read notify RECEIVED_SIGNAL_STREAM_ABORT
+write abort
+
+
+connect await RECEIVED_SIGNAL_STREAM_ABORT
+ "zilla://streams/kafka0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+
+write zilla:begin.ext ${kafka:beginEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .capabilities("PRODUCE_AND_FETCH")
+ .topic("mqtt-sessions")
+ .groupId("mqtt-clients")
+ .filter()
+ .header("type", "will-signal")
+ .build()
+ .filter()
+ .header("type", "expiry-signal")
+ .build()
+ .build()
+ .build()}
+
+connected
+
+read zilla:data.ext ${kafka:matchDataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+read ${mqtt:sessionSignal()
+ .expiry()
+ .instanceId("zilla-1")
+ .clientId("client-1")
+ .delay(1000)
+ .expireAt(expireAt)
+ .build()
+ .build()}
+
+write zilla:data.ext ${kafka:dataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1")
+ .hashKey("client-1")
+ .build()
+ .build()}
+write flush
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/server.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/server.rpt
new file mode 100644
index 0000000000..baaf9befa5
--- /dev/null
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.expiry.after.signal.stream.restart/server.rpt
@@ -0,0 +1,107 @@
+#
+# Copyright 2021-2023 Aklivity Inc
+#
+# Licensed under the Aklivity Community License (the "License"); you may not use
+# this file except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# https://www.aklivity.io/aklivity-community-license/
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+#
+
+property delayMillis 1000L
+property expireAt ${mqtt:timestamp() + delayMillis}
+
+accept "zilla://streams/kafka0"
+ option zilla:window 8192
+ option zilla:transmission "duplex"
+
+
+accepted
+
+read zilla:begin.ext ${kafka:matchBeginEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .capabilities("PRODUCE_AND_FETCH")
+ .topic("mqtt-sessions")
+ .groupId("mqtt-clients")
+ .filter()
+ .header("type", "will-signal")
+ .build()
+ .filter()
+ .header("type", "expiry-signal")
+ .build()
+ .build()
+ .build()}
+
+connected
+
+write option zilla:flags "init"
+write zilla:data.ext ${kafka:dataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+write [0x01 0x07 0x00 0x7a 0x69 0x6c 0x6c 0x61 0x2d 0x31 0x08 0x00 0x63 0x6c 0x69 0x65 0x6e 0x74 0x2d]
+write flush
+
+write abort
+read aborted
+
+accepted
+
+read zilla:begin.ext ${kafka:matchBeginEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .capabilities("PRODUCE_AND_FETCH")
+ .topic("mqtt-sessions")
+ .groupId("mqtt-clients")
+ .filter()
+ .header("type", "will-signal")
+ .build()
+ .filter()
+ .header("type", "expiry-signal")
+ .build()
+ .build()
+ .build()}
+
+connected
+
+write zilla:data.ext ${kafka:dataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1#expiry-signal")
+ .header("type", "expiry-signal")
+ .build()
+ .build()}
+write ${mqtt:sessionSignal()
+ .expiry()
+ .instanceId("zilla-1")
+ .clientId("client-1")
+ .delay(1000)
+ .expireAt(expireAt)
+ .build()
+ .build()}
+write flush
+
+# cleanup session state
+read zilla:data.ext ${kafka:matchDataEx()
+ .typeId(zilla:id("kafka"))
+ .merged()
+ .deferred(0)
+ .partition(-1, -1)
+ .key("client-1")
+ .hashKey("client-1")
+ .build()
+ .build()}
+read zilla:data.null
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/client.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/client.rpt
index a5e991862a..183ae31507 100644
--- a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/client.rpt
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/client.rpt
@@ -43,14 +43,9 @@ read zilla:data.ext ${kafka:matchDataEx()
.header("type", "expiry-signal")
.build()
.build()}
-read ${mqtt:sessionSignal()
- .expiry()
- .instanceId("zilla-1")
- .clientId("client-1")
- .delay(2000)
- .expireAt(expireAt)
- .build()
- .build()}
+read [0x01 0x07 0x00 0x7a 0x69 0x6c 0x6c 0x61 0x2d 0x31 0x08 0x00 0x63 0x6c 0x69 0x65 0x6e 0x74 0x2d]
+
+read [0x31 0xd0 0x07 0x00 0x00 0xbe 0x35 0x1a 0xa5 0x8a 0x01 0x00 0x00]
read zilla:data.ext ${kafka:matchDataEx()
.typeId(zilla:id("kafka"))
@@ -63,4 +58,3 @@ read zilla:data.ext ${kafka:matchDataEx()
.build()
.build()}
read zilla:data.null
-
diff --git a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/server.rpt b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/server.rpt
index 92d6b90b1f..fa11dc4ef9 100644
--- a/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/server.rpt
+++ b/specs/binding-mqtt-kafka.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/kafka/session.session.expiry.fragmented/server.rpt
@@ -13,14 +13,10 @@
# specific language governing permissions and limitations under the License.
#
-property delayMillis 2000L
-property expireAt ${mqtt:timestamp() + delayMillis}
-
accept "zilla://streams/kafka0"
option zilla:window 8192
option zilla:transmission "duplex"
-
accepted
read zilla:begin.ext ${kafka:matchBeginEx()
diff --git a/specs/binding-mqtt-kafka.spec/src/test/java/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/KafkaIT.java b/specs/binding-mqtt-kafka.spec/src/test/java/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/KafkaIT.java
index 12a4faa740..6e6328854c 100644
--- a/specs/binding-mqtt-kafka.spec/src/test/java/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/KafkaIT.java
+++ b/specs/binding-mqtt-kafka.spec/src/test/java/io/aklivity/zilla/specs/binding/mqtt/kafka/streams/KafkaIT.java
@@ -781,4 +781,22 @@ public void shouldRedirect() throws Exception
{
k3po.finish();
}
+
+ @Test
+ @Specification({
+ "${kafka}/session.session.expiry.fragmented/client",
+ "${kafka}/session.session.expiry.fragmented/server"})
+ public void shouldDecodeSessionExpirySignalFragmented() throws Exception
+ {
+ k3po.finish();
+ }
+
+ @Test
+ @Specification({
+ "${kafka}/session.expiry.after.signal.stream.restart/client",
+ "${kafka}/session.expiry.after.signal.stream.restart/server"})
+ public void shouldExpireSessionAfterSignalStreamRestart() throws Exception
+ {
+ k3po.finish();
+ }
}
diff --git a/specs/binding-mqtt.spec/pom.xml b/specs/binding-mqtt.spec/pom.xml
index 5f4a8d42f2..6779715e1c 100644
--- a/specs/binding-mqtt.spec/pom.xml
+++ b/specs/binding-mqtt.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-proxy.spec/pom.xml b/specs/binding-proxy.spec/pom.xml
index 12306c5159..4a9db2913f 100644
--- a/specs/binding-proxy.spec/pom.xml
+++ b/specs/binding-proxy.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-sse-kafka.spec/pom.xml b/specs/binding-sse-kafka.spec/pom.xml
index eb2a4a7e7d..51c1074e89 100644
--- a/specs/binding-sse-kafka.spec/pom.xml
+++ b/specs/binding-sse-kafka.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-sse.spec/pom.xml b/specs/binding-sse.spec/pom.xml
index ca154e69d3..66d8e11975 100644
--- a/specs/binding-sse.spec/pom.xml
+++ b/specs/binding-sse.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-tcp.spec/pom.xml b/specs/binding-tcp.spec/pom.xml
index ddff3d1ef3..c3d560f2ed 100644
--- a/specs/binding-tcp.spec/pom.xml
+++ b/specs/binding-tcp.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-tls.spec/pom.xml b/specs/binding-tls.spec/pom.xml
index 2e26afe0a2..cfb8f86b73 100644
--- a/specs/binding-tls.spec/pom.xml
+++ b/specs/binding-tls.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/binding-ws.spec/pom.xml b/specs/binding-ws.spec/pom.xml
index b107141797..afbad2bb8e 100644
--- a/specs/binding-ws.spec/pom.xml
+++ b/specs/binding-ws.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/engine.spec/pom.xml b/specs/engine.spec/pom.xml
index 58890540d1..07d7331bbf 100644
--- a/specs/engine.spec/pom.xml
+++ b/specs/engine.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/exporter-prometheus.spec/pom.xml b/specs/exporter-prometheus.spec/pom.xml
index 89ebe02efc..c92655b19c 100644
--- a/specs/exporter-prometheus.spec/pom.xml
+++ b/specs/exporter-prometheus.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/guard-jwt.spec/pom.xml b/specs/guard-jwt.spec/pom.xml
index 8c692dbaac..d6a1f460db 100644
--- a/specs/guard-jwt.spec/pom.xml
+++ b/specs/guard-jwt.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/metrics-grpc.spec/pom.xml b/specs/metrics-grpc.spec/pom.xml
index cfd7cef4c9..d47e0b70b1 100644
--- a/specs/metrics-grpc.spec/pom.xml
+++ b/specs/metrics-grpc.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/metrics-http.spec/pom.xml b/specs/metrics-http.spec/pom.xml
index 1c24c64d7a..8d799fb6ac 100644
--- a/specs/metrics-http.spec/pom.xml
+++ b/specs/metrics-http.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/metrics-stream.spec/pom.xml b/specs/metrics-stream.spec/pom.xml
index 28b9756740..37ff799137 100644
--- a/specs/metrics-stream.spec/pom.xml
+++ b/specs/metrics-stream.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/pom.xml b/specs/pom.xml
index b0def5bc9d..929d0cad30 100644
--- a/specs/pom.xml
+++ b/specs/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
zilla
- 0.9.57
+ 0.9.58
../pom.xml
diff --git a/specs/vault-filesystem.spec/pom.xml b/specs/vault-filesystem.spec/pom.xml
index 9c26243297..d685d28bd1 100644
--- a/specs/vault-filesystem.spec/pom.xml
+++ b/specs/vault-filesystem.spec/pom.xml
@@ -8,7 +8,7 @@
io.aklivity.zilla
specs
- 0.9.57
+ 0.9.58
../pom.xml