Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion allure-okhttp3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Use this module when your tests or test clients use OkHttp and you want request,

- Allure Java 3.x requires Java 17 or newer.
- This module targets the OkHttp `okhttp3` API used by OkHttp 3, 4, and 5.
- The current build validates against OkHttp 5.4.0.
- The current build validates against OkHttp 5.5.0.

## Installation

Expand Down Expand Up @@ -42,6 +42,21 @@ OkHttpClient client = new OkHttpClient.Builder()
.build();
```

## Server-Sent Events

OkHttp invokes server-sent event callbacks on reusable dispatcher threads. Use the Allure event-source factory so
steps and HTTP exchange attachments stay with the test or fixture that opens each source:

```java
EventSource.Factory eventSourceFactory = AllureEventSources.createFactory(client);

EventSource eventSource = eventSourceFactory.newEventSource(request, listener);
```

The returned factory is reusable. It captures context when `newEventSource` is called, so the client, factory, request,
and listener may all be constructed before the owning test or fixture starts. When no Allure test or fixture is
current, callbacks run without an Allure owner instead of inheriting stale context from an OkHttp dispatcher thread.

## Report Output

- Request method, URL, headers, and body when available.
Expand Down
1 change: 1 addition & 0 deletions allure-okhttp3/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ val okhttpVersion = "5.5.0"
dependencies {
api(project(":allure-java-commons"))
compileOnly("com.squareup.okhttp3:okhttp:$okhttpVersion")
compileOnly("com.squareup.okhttp3:okhttp-sse:$okhttpVersion")
testImplementation("org.wiremock:wiremock")
testImplementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
testImplementation("com.squareup.okhttp3:okhttp-sse:$okhttpVersion")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.qameta.allure.okhttp3;

import io.qameta.allure.AllureThreadBinding;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import okhttp3.sse.EventSources;

import java.util.Objects;

/**
* Allure-aware server-sent event source factories for OkHttp.
*/
public final class AllureEventSources {

private AllureEventSources() {
throw new IllegalStateException("Do not instance");
}

/**
* Creates a reusable event source factory that captures the current Allure test or fixture when each event source
* is opened. The captured context is restored for the HTTP interceptor and every listener callback.
*
* <p>The client, factory, request, and listener may be created before a test or fixture starts. Context is captured
* only when {@link EventSource.Factory#newEventSource(Request, EventSourceListener)} is called.</p>
*
* @param client the OkHttp client
* @return the context-aware event source factory
*/
public static EventSource.Factory createFactory(final OkHttpClient client) {
final EventSource.Factory delegate = EventSources.createFactory(Objects.requireNonNull(client, "client"));
return (request, listener) -> {
final AllureOkHttp3Context context = AllureOkHttp3Context.capture();
final Request contextRequest = Objects.requireNonNull(request, "request")
.newBuilder()
.tag(AllureOkHttp3Context.class, context)
.build();
final EventSourceListener contextListener = new ContextEventSourceListener(
Objects.requireNonNull(listener, "listener"),
context
);
return delegate.newEventSource(contextRequest, contextListener);
};
}

private static final class ContextEventSourceListener extends EventSourceListener {

private final EventSourceListener delegate;
private final AllureOkHttp3Context context;

private ContextEventSourceListener(final EventSourceListener delegate, final AllureOkHttp3Context context) {
this.delegate = delegate;
this.context = context;
}

@Override
public void onOpen(final EventSource eventSource, final Response response) {
runInContext(() -> delegate.onOpen(eventSource, response));
}

@Override
public void onEvent(final EventSource eventSource, final String id,
final String type, final String data) {
runInContext(() -> delegate.onEvent(eventSource, id, type, data));
}

@Override
public void onClosed(final EventSource eventSource) {
runInContext(() -> delegate.onClosed(eventSource));
}

@Override
public void onFailure(final EventSource eventSource, final Throwable throwable,
final Response response) {
runInContext(() -> delegate.onFailure(eventSource, throwable, response));
}

private void runInContext(final Runnable callback) {
try (AllureThreadBinding ignored = context.bind()) {
callback.run();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.qameta.allure.okhttp3;

import io.qameta.allure.Allure;
import io.qameta.allure.AllureThreadBinding;
import io.qameta.allure.http.HttpExchange;
import io.qameta.allure.http.HttpExchangeBody;
import io.qameta.allure.http.HttpExchangeError;
Expand Down Expand Up @@ -63,13 +64,23 @@ public AllureOkHttp3 configureHttpExchange(final Consumer<HttpExchange.Builder>
*/
@Override
public Response intercept(final Chain chain) throws IOException {
final Request request = chain.request();
final AllureOkHttp3Context context = request.tag(AllureOkHttp3Context.class);
if (Objects.isNull(context)) {
return interceptInCurrentContext(chain, request);
}
try (AllureThreadBinding ignored = context.bind()) {
return interceptInCurrentContext(chain, request);
}
}

private Response interceptInCurrentContext(final Chain chain, final Request request) throws IOException {
// enrichment-only integration: pass the call through untouched when no executable is
// running — no warnings, no request/response body buffering
if (Allure.getLifecycle().getCurrentExecutableKey().isEmpty()) {
return chain.proceed(chain.request());
return chain.proceed(request);
}
final long start = System.currentTimeMillis();
final Request request = chain.request();
final HttpExchangeRequest.Builder requestBuilder = HttpExchangeRequest
.builder(request.method(), request.url().toString())
.addHeaders(toNameValues(request.headers().toMultimap()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2016-2026 Qameta Software Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.qameta.allure.okhttp3;

import io.qameta.allure.Allure;
import io.qameta.allure.AllureExternalKey;
import io.qameta.allure.AllureLifecycle;
import io.qameta.allure.AllureThreadBinding;

final class AllureOkHttp3Context {

private static final AllureExternalKey NO_OWNER = AllureExternalKey.random(AllureOkHttp3Context.class);

private final AllureLifecycle lifecycle;
private final AllureExternalKey owner;

private AllureOkHttp3Context(final AllureLifecycle lifecycle, final AllureExternalKey owner) {
this.lifecycle = lifecycle;
this.owner = owner;
}

static AllureOkHttp3Context capture() {
final AllureLifecycle lifecycle = Allure.getLifecycle();
final AllureExternalKey owner = lifecycle.getCurrentRootKey().orElse(NO_OWNER);
return new AllureOkHttp3Context(lifecycle, owner);
}

AllureThreadBinding bind() {
return lifecycle.bindDetached(owner);
}
}
Loading