@@ -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.
188259pub struct GrpcInterceptor {
0 commit comments