-
Notifications
You must be signed in to change notification settings - Fork 391
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
Remove warning header; Configurable generated error responses (#2955) #3050
Conversation
attemptFastWrite(ctx, Response.fromThrowable(throwable)) | ||
attemptFastWrite( | ||
ctx, | ||
Response.fromThrowable(throwable, runtime.unsafeRunSync(ErrorResponseConfig.configRef.get)), |
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.
It seems a bit wasteful to start a fiber only to extract a FiberRef
from it.
How about adding this method to NettyRuntime
:
def getRef[A](ref: FiberRef[A]): A = zioRuntime.fiberRefs.getOrDefault(ref)
And then this becomes:
Response.fromThrowable(throwable, runtime.getRef(ErrorResponseConfig.configRef))
@@ -58,6 +58,9 @@ final case class Response( | |||
self.copy(body = Body.fromChunk(bytes)) | |||
} | |||
|
|||
def contentType(json: MediaType): Response = | |||
self.addHeader(Header.ContentType(json)) |
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.
Rendering of the ContentType
header is quite expensive. For this reason, at the bottom of the the Response
companion object the main 3 content-types are cached after calling untyped
on them (which invokes the render
method).
Might be better to pattern match here and check if the media type here matches one of those 3 and use the cached value or fallback to Header.ContentType.apply
if not?
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.
Also the param name should be changed to mediaType
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.
Maybe it is just the best to use the untyped API. Probably the cheapest.
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.
Apologies, I missed some minor things in my last review. Otherwise LGTM
withErrorBody: Boolean = false, | ||
withStackTrace: Boolean = false, | ||
maxStackTraceDepth: Int = 10, | ||
errorFormat: ErrorResponseConfig.ErrorFormat = ErrorResponseConfig.ErrorFormat.Html, |
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 thinking plaintext might make more sense as a default as it accounts for a wider range of usecases (APIs & serving user content)
Middleware.runBefore(configRef.updateSome { case oldConfig if oldConfig != config => config }) | ||
|
||
private[http] lazy val configRef: FiberRef[ErrorResponseConfig] = | ||
FiberRef.unsafe.make(ErrorResponseConfig())(Unsafe) |
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.
For performance reasons, FiberRefs
use reference equality to determine whether a FiberRef
was changed. It's probably not an issue but perhaps use default
here just incase
ErrorResponseConfig.configRef.get.map { cfg => | ||
if (cfg.withErrorBody) { | ||
val mediaType: MediaType = accept | ||
.flatMap(_.mimeTypes.sortBy(mt => -mt.qFactor.getOrElse(1.0)).map(_.mediaType).collectFirst { |
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.
Should we add an implicit Ordering
in the companion object of Accept
? 🤔
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.
We could also ensure it is always sorted.
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.
Best to ensure it's always sorted, we don't want to be doing this on each request.
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.
It has to be done on each request, since this the the requests accept header. But since the amount of accepted media types will be small, I thought a sorted list might be easier to understand. It is just sorted by q factor when ever you deal with the typed interface.
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.
Maybe you could check and avoid sorting it if it's already sorted, which would avoid the overhead entirely for the happy path.
@@ -58,6 +58,9 @@ final case class Response( | |||
self.copy(body = Body.fromChunk(bytes)) | |||
} | |||
|
|||
def contentType(mediaType: MediaType): Response = | |||
self.addHeader("content-type", mediaType.fullType) |
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.
Outside the scope of this PR, but I'm wondering whether we should change the fullType
to a val
instead of a lazy val
to avoid the penalty of lazy vals.
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.
Since both are static strings, I'll expect the val to be cheap anyway
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 this would be scala 3 only, inline def is probably best?
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.
Let's tackle this in another issue/PR where we can revise the usages of lazy val
in ZIO HTTP. I think there are quite a few unnecessary ones
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 (config.withStackTrace && throwable.getStackTrace.nonEmpty) | ||
(if (config.maxStackTraceDepth == 0) throwable.getStackTrace | ||
else throwable.getStackTrace.take(config.maxStackTraceDepth)) | ||
.mkString("\n", "\n", "") | ||
else "" |
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.
Is getStackTrace
cheap? Or should we rewrite this to invoke it only once?
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'll double check
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 created a local val. I think we could optimize printing limited stack traces, since getStackTrace
captures the full stack. We could probably walk the trace only until n. But I think this is okay for now.
|
||
final def handleErrorZIO(f: Err => ZIO[Any, Nothing, Response])(implicit trace: Trace): Route[Env, Nothing] = | ||
self.handleErrorCauseZIO { cause => | ||
cause.failureOrCause match { | ||
case Left(err) => f(err) | ||
case Right(cause) => ZIO.succeed(Response.fromCause(cause)) | ||
case Right(cause) => ErrorResponseConfig.configRef.get.map(Response.fromCause(cause, _)) |
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.
Not as nice, but it requires 1 FlatMap less:
case Right(cause) => ErrorResponseConfig.configRef.get.map(Response.fromCause(cause, _)) | |
case Right(cause) => ErrorResponseConfig.configRef.getWith(c => ZIO.succeed(Response.fromCause(cause, c))) |
There are a few other places this can be applied to
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.
Still nice enough
* maximum number of stack trace lines to include in the response body. Set to | ||
* 0 to include all lines. | ||
*/ | ||
case class ErrorResponseConfig( |
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.
case class ErrorResponseConfig( | |
final case class ErrorResponseConfig( |
@@ -549,7 +547,7 @@ sealed trait Handler[-R, +Err, -In, +Out] { self => | |||
self(in) | |||
|
|||
final def sandbox(implicit trace: Trace): Handler[R, Response, In, Out] = | |||
self.mapErrorCause(Response.fromCause(_)) | |||
self.mapErrorCauseZIO(c => ErrorResponseConfig.configRef.get.map(Response.fromCause(c, _)).flip) |
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.
There's a fair amount of overhead here--of course, there was before, but now there is even more.
Could be useful to unsafely get the fiber ref value in as many places as you can "safely" bundle it up in lazy evaluation (like here).
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 tried to figure out, what you mean. But I don't get it.
Currently, this config is saved within a fiber ref. Therefore idk how to get it unsafe. Also idk if the call to sandbox needs to be that optimized? Don't we advertise this as a non-prod feature?
But what you try to say might be true for other places?
One other design option I thought of was, to use a ConcurrentHashMap with the route pattern as a key. That way we could avoid fiber refs and zio based access to our config storage.
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.
Ah right, FiberRef instead of Ref.
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.
Wondering what the thought in using a fiber ref is? To make it compatible with scoped changes?
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.
Yes. That you can override the behavior per route/request.
3188189
to
8abbbee
Compare
1a0595c
to
6f6bbd2
Compare
6f6bbd2
to
ab9fdf7
Compare
Throwable
through internal code and certain user calls, likesandbox
. The basic idea is, that the user can auto generate error responses and based on config the details are shown or not. The config is saved in a fiber ref. That makes it possible to not only set a global default, but override config based on the route or even the request. I added for that some middleware to support this in an easy way. For example, the following code will change the config based on a set header, to get maximum details for the error.fixes #2955
fixes #2701
fixes #2948
/claim #2955
/claim #2701
/claim #2948