-
-
Notifications
You must be signed in to change notification settings - Fork 251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SSE support #2126
SSE support #2126
Changes from 3 commits
278d761
7c7725f
f206bbf
11a08cd
448c5e3
53a3450
1a1f613
e8addcb
0a433ab
d7fb211
79bcfec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,18 @@ | ||
package caliban.interop.tapir | ||
|
||
import caliban.ResponseValue.StreamValue | ||
import caliban._ | ||
import caliban.ResponseValue.{ ObjectValue, StreamValue } | ||
import caliban.wrappers.Caching | ||
import sttp.capabilities.zio.ZioStreams | ||
import sttp.capabilities.zio.ZioStreams.Pipe | ||
import sttp.capabilities.{ Streams, WebSockets } | ||
import sttp.model.{ headers => _, _ } | ||
import sttp.model.sse.ServerSentEvent | ||
import sttp.monad.MonadError | ||
import sttp.tapir.Codec.JsonCodec | ||
import sttp.tapir.model.ServerRequest | ||
import sttp.tapir.server.ServerEndpoint | ||
import sttp.tapir.ztapir.ZioServerSentEvents | ||
import sttp.tapir.{ headers, _ } | ||
import zio._ | ||
import zio.stream.ZStream | ||
|
@@ -89,6 +91,10 @@ object TapirAdapter { | |
oneOfVariantValueMatcher[CalibanBody.Stream[stream.BinaryStream]]( | ||
streamTextBody(stream)(CodecFormat.Json(), Some(StandardCharsets.UTF_8)).toEndpointIO | ||
.map(Right(_)) { case Right(value) => value } | ||
) { case Right(_) => true }, | ||
oneOfVariantValueMatcher[CalibanBody.Stream[stream.BinaryStream]]( | ||
streamBinaryBody(stream)(CodecFormat.TextEventStream()).toEndpointIO | ||
.map(Right(_)) { case Right(value) => value } | ||
) { case Right(_) => true } | ||
) | ||
|
||
|
@@ -114,6 +120,8 @@ object TapirAdapter { | |
case _ => false | ||
} | ||
) | ||
val isSSE = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could make this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding to this, we could probably check the Accept header just a single time (we currently check it once for |
||
request.header(HeaderNames.Accept).map(v => v.contains(MediaType.TextEventStream.toString())).getOrElse(false) | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
response match { | ||
case resp @ GraphQLResponse(StreamValue(stream), _, _, _) => | ||
( | ||
|
@@ -142,17 +150,27 @@ object TapirAdapter { | |
val code = | ||
response.errors.collectFirst { case HttpRequestMethod.MutationOverGetError => StatusCode.BadRequest } | ||
.getOrElse(StatusCode.Ok) | ||
val cacheDirective = HttpUtils.computeCacheDirective(response.extensions) | ||
( | ||
MediaType.ApplicationJson, | ||
code, | ||
cacheDirective, | ||
encodeSingleResponse( | ||
resp, | ||
keepDataOnErrors = true, | ||
excludeExtensions = cacheDirective.map(_ => Set(Caching.DirectiveName)) | ||
) | ||
) | ||
val cacheDirective = computeCacheDirective(response.extensions) | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
isSSE match { | ||
case true => | ||
( | ||
MediaType.TextEventStream, | ||
code, | ||
cacheDirective, | ||
encodeTextEventStreamResponse(resp) | ||
) | ||
case false => | ||
( | ||
MediaType.ApplicationJson, | ||
code, | ||
cacheDirective, | ||
encodeSingleResponse( | ||
resp, | ||
keepDataOnErrors = true, | ||
excludeExtensions = cacheDirective.map(_ => Set(Caching.DirectiveName)) | ||
) | ||
) | ||
} | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
|
@@ -199,6 +217,37 @@ object TapirAdapter { | |
) | ||
} | ||
|
||
private def encodeTextEventStreamResponse[E, BS]( | ||
resp: GraphQLResponse[E] | ||
)(implicit streamConstructor: StreamConstructor[BS], responseCodec: JsonCodec[ResponseValue]): CalibanBody[BS] = { | ||
val response: ZStream[Any, Throwable, ServerSentEvent] = resp.data match { | ||
case ObjectValue(fields) => | ||
fields.foldLeft(ZStream.empty: ZStream[Any, Throwable, ServerSentEvent]) { case (_, v) => | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
v match { | ||
case (fieldName, StreamValue(stream)) => | ||
stream.map { r => | ||
ServerSentEvent( | ||
Some( | ||
responseCodec.encode( | ||
GraphQLResponse( | ||
ObjectValue(List(fieldName -> r)), | ||
resp.errors | ||
).toResponseValue | ||
) | ||
), | ||
Some("next") | ||
) | ||
}.orDie | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case _ => | ||
ZStream.succeed(ServerSentEvent(Some(responseCodec.encode(resp.toResponseValue)), Some("next"))) | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
case _ => | ||
ZStream.succeed(ServerSentEvent(Some(responseCodec.encode(resp.toResponseValue)), Some("next"))) | ||
kyri-petrou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
Right(streamConstructor(ZioServerSentEvents.serialiseSSEToBytes(response))) | ||
} | ||
|
||
private def encodeSingleResponse[E]( | ||
response: GraphQLResponse[E], | ||
keepDataOnErrors: Boolean, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm quite surprised this works since there is a
Right(_) => true
above this, does the selector take into account the Accepts headers when computing the match?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly don't know if it does take the Accept header into account but it does work. If we can do it in another way that is more readable or so I'm all for it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If memory serves well, I also came across this some time ago and I was also surprised about it. I think I also concluded that it takes the Accept header into account