Skip to content

Commit 4149885

Browse files
committed
Add diagnostic logging for non-gRPC HTTP responses
Intercepts raw HTTP responses before tonic-web gRPC-web framing to capture the actual response body when the server returns a non-gRPC response. This helps diagnose 'Invalid header bit N expected 0 or 1' errors by logging the HTTP status, content-type, and response body preview.
1 parent e4beaca commit 4149885

1 file changed

Lines changed: 71 additions & 0 deletions

File tree

libsql/src/replication/client.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ impl GrpcChannel {
140140
.pool_idle_timeout(None)
141141
.pool_max_idle_per_host(3)
142142
.build(connector);
143+
let client = DiagnosticResponseService::new(client);
143144
let client = GrpcWebClientService::new(client);
144145

145146
let classifier = GrpcErrorsAsFailures::new().with_success(GrpcCode::FailedPrecondition);
@@ -183,6 +184,76 @@ impl Service<http::Request<BoxBody>> for GrpcChannel {
183184
}
184185
}
185186

187+
/// Diagnostic middleware that intercepts raw HTTP responses before gRPC-web
188+
/// framing. When the response status is not 200 or the content-type is not
189+
/// grpc-web, it buffers and logs the response body so we can diagnose
190+
/// "Invalid header bit N" errors from tonic-web.
191+
#[derive(Clone)]
192+
struct DiagnosticResponseService<S> {
193+
inner: S,
194+
}
195+
196+
impl<S> DiagnosticResponseService<S> {
197+
fn new(inner: S) -> Self {
198+
Self { inner }
199+
}
200+
}
201+
202+
impl<S, ReqBody> Service<http::Request<ReqBody>> for DiagnosticResponseService<S>
203+
where
204+
S: Service<http::Request<ReqBody>, Response = http::Response<hyper::Body>, Error = hyper::Error>
205+
+ Clone
206+
+ Send
207+
+ 'static,
208+
S::Future: Send + 'static,
209+
ReqBody: Send + 'static,
210+
{
211+
type Response = http::Response<hyper::Body>;
212+
type Error = hyper::Error;
213+
type Future =
214+
Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
215+
216+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
217+
self.inner.poll_ready(cx)
218+
}
219+
220+
fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
221+
let uri = req.uri().clone();
222+
let fut = self.inner.call(req);
223+
Box::pin(async move {
224+
let resp = fut.await?;
225+
let status = resp.status();
226+
let content_type = resp
227+
.headers()
228+
.get(http::header::CONTENT_TYPE)
229+
.and_then(|v| v.to_str().ok())
230+
.unwrap_or("<none>")
231+
.to_string();
232+
let is_grpc = content_type.contains("grpc");
233+
234+
if status != http::StatusCode::OK || !is_grpc {
235+
// Buffer the body to log it, then re-create the response
236+
let (parts, body) = resp.into_parts();
237+
let body_bytes = hyper::body::to_bytes(body).await.unwrap_or_default();
238+
let preview_len = std::cmp::min(body_bytes.len(), 1024);
239+
let body_preview = String::from_utf8_lossy(&body_bytes[..preview_len]);
240+
tracing::warn!(
241+
status = %status,
242+
uri = %uri,
243+
content_type = %content_type,
244+
body_len = body_bytes.len(),
245+
body_preview = %body_preview,
246+
"[libsql diagnostic] non-gRPC HTTP response — will cause 'Invalid header bit' error"
247+
);
248+
Ok(http::Response::from_parts(parts, hyper::Body::from(body_bytes)))
249+
} else {
250+
tracing::trace!(status = %status, uri = %uri, "[libsql diagnostic] gRPC response OK");
251+
Ok(resp)
252+
}
253+
})
254+
}
255+
}
256+
186257
#[derive(Clone)]
187258
/// Contains token and namespace headers to append to every request.
188259
pub struct GrpcInterceptor {

0 commit comments

Comments
 (0)