Spikard Troubleshooting Guide¶
This comprehensive guide covers common issues encountered when working with Spikard across OpenAPI, GraphQL, and Protobuf/gRPC protocols. Solutions are organized by problem category with clear error examples, root causes, and prevention strategies.
Table of Contents¶
- Schema Validation Errors
- Code Generation Failures
- Quality Validation Failures
- Integration Issues
- Runtime Errors
- gRPC-Specific Issues
Schema Validation Errors¶
Problem 1: OpenAPI - Missing Required Field in Request Body¶
Error Message:
{
"type": "https://spikard.dev/errors/validation-error",
"title": "Request Validation Failed",
"status": 422,
"detail": "1 validation error in request",
"errors": [
{
"type": "missing",
"loc": ["body", "username"],
"msg": "Field required",
"input": {}
}
]
}
Root Cause:
The client is submitting a request missing a field marked as required in the OpenAPI schema. Spikard validates all incoming requests against the schema before they reach your handler.
Solution:
- Check your OpenAPI schema definition:
components:
schemas:
CreateUserRequest:
type: object
required:
- username # This field is mandatory
- email
properties:
username:
type: string
email:
type: string
- Ensure the client includes all required fields:
// TypeScript client
const response = await fetch("/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: "johndoe", // Include required field
email: "john@example.com",
}),
});
Prevention Tips:
- Use OpenAPI documentation generators to share API contracts with clients
- Enable request validation in development to catch issues early
- Consider providing default values for optional fields in your schema
- Use schema examples in your OpenAPI spec to document expected inputs
Problem 2: GraphQL - Invalid Query Syntax¶
Error Message:
{
"errors": [
{
"message": "GraphQL parse error: Syntax error near line 3",
"extensions": {
"code": "GRAPHQL_PARSE_ERROR",
"status": 400,
"type": "https://spikard.dev/errors/graphql-parse-error"
}
}
]
}
Root Cause: The GraphQL query contains syntax errors such as mismatched braces, missing colons, or invalid field names.
Solution:
Check for common syntax issues:
# INCORRECT - missing closing brace
query GetUser {
user(id: "123") {
id
name
email
# CORRECT
query GetUser {
user(id: "123") {
id
name
email
}
}
Prevention Tips:
- Use GraphQL IDE tools (GraphiQL, GraphQL Playground) with syntax highlighting
- Enable query validation in your GraphQL client
- Use typed GraphQL clients (like Apollo Client with code generation)
- Add unit tests for your GraphQL queries
Problem 3: Protobuf - Message Type Not Found¶
Error Message:
Root Cause: The protobuf message type referenced in your code doesn't exist in the compiled descriptor set, usually due to incorrect imports or package names.
Solution:
- Verify your
.protofile package and message definitions:
syntax = "proto3";
package example.v1; // Package must match
message UserRequest {
string id = 1;
string name = 2;
}
- Ensure you're importing the correct generated code:
// TypeScript
import { UserRequest } from "./gen/example/v1/user";
const request = UserRequest.create({ id: "123", name: "John" });
Prevention Tips:
- Use consistent package naming across all
.protofiles - Regenerate protobuf code after schema changes
- Verify import paths match your project structure
- Use
protoc --descriptor_set_outto debug descriptor issues
Problem 4: String Length Constraint Violation¶
Error Message:
{
"type": "https://spikard.dev/errors/validation-error",
"status": 422,
"errors": [
{
"type": "string_too_short",
"loc": ["body", "password"],
"msg": "String should have at least 8 characters",
"ctx": { "min_length": 8 }
}
]
}
Root Cause: Input string doesn't meet the minimum or maximum length constraints defined in the schema.
Solution:
- Check schema constraints:
- Validate input before submission:
# Python
def validate_password(password: str) -> bool:
if len(password) < 8:
raise ValueError("Password must be at least 8 characters")
if len(password) > 128:
raise ValueError("Password must not exceed 128 characters")
return True
Prevention Tips:
- Display constraint information in UI forms
- Implement client-side validation matching server rules
- Provide clear error messages indicating the constraints
- Use appropriate input types (e.g., password fields with length indicators)
Problem 5: Enum Value Not in Allowed List¶
Error Message:
{
"type": "enum",
"loc": ["body", "status"],
"msg": "Input should be 'active', 'inactive' or 'pending'",
"ctx": { "expected": "'active', 'inactive' or 'pending'" }
}
Root Cause: The provided value doesn't match any of the allowed enum values defined in the schema.
Solution:
- Review allowed enum values:
- Use type-safe enums in code:
// TypeScript
enum UserStatus {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}
const request = {
status: UserStatus.Active, // Type-safe
};
# Python
from enum import Enum
class UserStatus(str, Enum):
ACTIVE = "active"
INACTIVE = "inactive"
PENDING = "pending"
request = {"status": UserStatus.ACTIVE.value}
Prevention Tips:
- Generate client code from schemas to get type-safe enums
- Use autocomplete-enabled dropdowns in UI
- Document enum values in API documentation
- Consider using constants instead of magic strings
Code Generation Failures¶
Problem 6: TypeScript - Module Resolution Failure¶
Error Message:
Root Cause: Generated TypeScript files aren't in the expected location or the module resolution configuration is incorrect.
Solution:
- Verify code generation succeeded:
- Check
tsconfig.jsonpaths configuration:
- Verify generated files exist:
Prevention Tips:
- Add generated code directories to
.gitignore - Use consistent output paths across the team
- Document code generation steps in README
- Add code generation to CI/CD pipeline
Problem 7: Ruby - Syntax Error in Generated Code¶
Error Message:
Root Cause: The code generator produced invalid Ruby syntax, often due to schema edge cases or special characters in field names.
Solution:
- Check for reserved keywords or special characters:
- Regenerate code with proper escaping:
- If the issue persists, report the schema pattern to the Spikard team.
Prevention Tips:
- Avoid using language-reserved keywords in schema field names
- Use
snake_casefor field names consistently - Run syntax validation immediately after generation
- Enable strict validation mode during generation
Problem 8: Python - Import Error for Generated Protobuf¶
Error Message:
Root Cause:
The gRPC Python code wasn't generated or is out of sync with the .proto files.
Solution:
- Regenerate gRPC Python code:
python -m grpc_tools.protoc \
--proto_path=. \
--python_out=./gen \
--grpc_python_out=./gen \
user.proto
- Verify both
*_pb2.pyand*_pb2_grpc.pyfiles exist:
- Check import paths:
# Use relative imports if in package
from .gen import user_pb2, user_pb2_grpc
# Or absolute imports
from gen import user_pb2, user_pb2_grpc
Prevention Tips:
- Add protobuf compilation to build scripts
- Use consistent proto file organization
- Version control
.protofiles, not generated code - Document gRPC setup in project README
Quality Validation Failures¶
Problem 9: Python - pyrefly Type Error¶
Error Message:
Root Cause: The handler returns a plain dictionary instead of the typed response model, causing pyrefly strict mode to fail.
Solution:
Use proper type annotations and return types:
# INCORRECT
def get_user(user_id: str):
return {"id": user_id, "name": "John"}
# CORRECT
from msgspec import Struct
class UserResponse(Struct):
id: str
name: str
def get_user(user_id: str) -> UserResponse:
return UserResponse(id=user_id, name="John")
Prevention Tips:
- Enable pyrefly in pre-commit hooks
- Use
preset = "strict"in[tool.pyrefly]during development - Generate type stubs from OpenAPI schemas
- Run type checking in CI/CD
Problem 10: TypeScript - tsc Compilation Error¶
Error Message:
Root Cause: Nullable fields in the schema aren't handled with proper type guards in TypeScript.
Solution:
Add proper null checks:
// INCORRECT
function processUser(user: User) {
const name: string = user.name; // user.name might be undefined
console.log(name.toUpperCase());
}
// CORRECT
function processUser(user: User) {
if (user.name) {
const name: string = user.name;
console.log(name.toUpperCase());
} else {
console.log("Name not provided");
}
}
// OR use optional chaining
function processUser(user: User) {
console.log(user.name?.toUpperCase() ?? "Name not provided");
}
Prevention Tips:
- Enable
strictNullChecksintsconfig.json - Use non-nullable types in schemas where appropriate
- Apply consistent null-handling patterns
- Use TypeScript 4.4+ for improved type narrowing
Problem 11: Ruby - Steep Type Mismatch¶
Error Message:
Root Cause: Optional fields in the schema produce nullable types in Ruby, requiring explicit nil handling.
Solution:
Use proper type signatures and nil checks:
# Add Steep type signature
# @type var user: User
# @type var name: String
user = get_user(id)
# INCORRECT
name = user.name
puts name.upcase
# CORRECT
if user.name
name = user.name
puts name.upcase
else
puts "Name not available"
end
Prevention Tips:
- Use RBS type definitions
- Enable Steep in CI pipeline
- Use Sorbet for runtime type checking
- Mark required fields explicitly in schemas
Problem 12: PHP - PHPStan Level Max Error¶
Error Message:
Root Cause: Generated PHP code lacks proper type hints, failing PHPStan's strict analysis.
Solution:
Ensure schema includes type information:
# OpenAPI schema
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
age:
type: integer
minimum: 0
Generated code will include types:
<?php
class User
{
public function __construct(
public string $id,
public string $email,
public int $age,
) {}
}
Prevention Tips:
- Use PHP 8.1+ for property types
- Specify formats in OpenAPI schema
- Run PHPStan with
--level=maxlocally - Keep generated code in separate namespace
Problem 13: Rust - Clippy Warnings as Errors¶
Error Message:
Root Cause: Generated Rust code violates Clippy's complexity limits, often due to handlers with many parameters.
Solution:
Refactor to use request objects:
// INSTEAD OF
fn handle_user(
id: String,
name: String,
email: String,
age: i32,
// ... 8 parameters
) -> Result<Response> {
// handler
}
// USE
#[derive(Deserialize)]
struct UserRequest {
id: String,
name: String,
email: String,
age: i32,
}
fn handle_user(req: UserRequest) -> Result<Response> {
// handler
}
Prevention Tips:
- Group related parameters into structs
- Use the builder pattern for complex requests
- Run
cargo clippybefore committing - Configure Clippy to allow specific warnings if needed
Integration Issues¶
Problem 14: CORS Preflight Request Failing¶
Error Message (Browser Console):
Access to fetch at 'https://api.example.com/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass access
control check: No 'Access-Control-Allow-Origin' header is present.
Root Cause: The server doesn't respond to OPTIONS requests or doesn't include required CORS headers.
Solution:
Configure CORS middleware:
# Python
from spikard import App
app = App()
app.config.cors.enabled = True
app.config.cors.allowed_origins = ["http://localhost:3000"]
app.config.cors.allowed_methods = ["GET", "POST", "PUT", "DELETE"]
app.config.cors.allowed_headers = ["Content-Type", "Authorization"]
// TypeScript
import { App } from "@spikard/node";
const app = new App();
app.config({
cors: {
enabled: true,
allowedOrigins: ["http://localhost:3000"],
allowedMethods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
},
});
# Ruby
require 'spikard'
app = Spikard::App.new
app.config.cors.enabled = true
app.config.cors.allowed_origins = ['http://localhost:3000']
app.config.cors.allowed_methods = ['GET', 'POST', 'PUT', 'DELETE']
Prevention Tips:
- Enable CORS in development mode
- Use environment-specific CORS configurations
- Test with actual frontend URLs, not just curl
- Log CORS headers in development
Problem 15: Authentication Token Validation Failure¶
Error Message:
{
"type": "https://spikard.dev/errors/authentication-error",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or expired authentication token"
}
Root Cause: The JWT token is expired, malformed, or signed with wrong secret.
Solution:
- Verify token structure and expiration:
# Python
import jwt
from datetime import datetime
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
key="your-secret-key",
algorithms=["HS256"]
)
# Check expiration
if payload.get('exp', 0) < datetime.now().timestamp():
raise ValueError("Token expired")
return payload
except jwt.InvalidTokenError as e:
raise ValueError(f"Invalid token: {e}")
- Include valid token in requests:
// TypeScript
const response = await fetch("/api/users", {
headers: {
Authorization: `Bearer ${validToken}`,
},
});
Prevention Tips:
- Use standard JWT libraries (don't roll your own)
- Set reasonable token expiration times
- Implement token refresh mechanisms
- Log authentication failures for debugging
- Use environment variables for secrets
Problem 16: Request Body Too Large¶
Error Message:
{
"type": "https://spikard.dev/errors/bad-request",
"title": "Request Entity Too Large",
"status": 413,
"detail": "Request body exceeds maximum size of 1MB"
}
Root Cause: The request payload exceeds the configured body size limit.
Solution:
- Increase body size limit if appropriate:
- Or use streaming for large uploads:
# Python
from spikard import UploadFile
async def upload_file(file: UploadFile):
# Stream directly to storage
await storage.save(file.filename, file.stream)
Prevention Tips:
- Set appropriate limits for your use case
- Use streaming for large file uploads
- Implement chunked uploads for very large files
- Document size limits in API documentation
Runtime Errors¶
Problem 17: Handler Timeout¶
Error Message:
{
"type": "https://spikard.dev/errors/internal-server-error",
"title": "Gateway Timeout",
"status": 504,
"detail": "Handler execution exceeded timeout of 30s"
}
Root Cause: The handler takes longer than the configured timeout to execute, usually due to slow database queries or external API calls.
Solution:
- Optimize slow operations:
# SLOW - sequential database calls
async def get_user_data(user_id: str):
user = await db.get_user(user_id)
posts = await db.get_posts(user_id)
comments = await db.get_comments(user_id)
return {"user": user, "posts": posts, "comments": comments}
# FAST - parallel database calls
import asyncio
async def get_user_data(user_id: str):
user, posts, comments = await asyncio.gather(
db.get_user(user_id),
db.get_posts(user_id),
db.get_comments(user_id)
)
return {"user": user, "posts": posts, "comments": comments}
- Or increase timeout for specific endpoints:
- Use background task configuration:
Consider using external task queues (Redis, RabbitMQ, or cloud providers) for deferred processing, as background tasks must complete within the request timeout window.
Prevention Tips:
- Profile handler performance
- Add database query indexes
- Use caching for frequently accessed data
- Implement request queuing for expensive operations
- Set realistic timeout values
Problem 18: Dependency Injection Failure¶
Error Message:
Root Cause: The DI container doesn't have a provider for a required dependency.
Solution:
Manage dependencies via route-level handlers:
# Python
from spikard import App
class Database:
def __init__(self, url: str):
self.url = url
app = App()
db = Database(url="postgresql://localhost/mydb")
async def get_user(request):
user_id = request.path_params.get("id")
return await db.query(f"SELECT * FROM users WHERE id = {user_id}")
app.get("/users/{id}", get_user)
// TypeScript
import { App } from "@spikard/node";
class Database {
constructor(private url: string) {}
}
const app = new App();
const db = new Database("postgresql://localhost/mydb");
app.get("/users/:id", async (req) => {
const userId = req.params.id;
return await db.query(`SELECT * FROM users WHERE id = ${userId}`);
});
Prevention Tips:
- Register all providers at application startup
- Use dependency interfaces for testability
- Validate DI configuration before deployment
- Document required dependencies
Problem 19: Streaming Response Connection Closed¶
Error Message (Logs):
Root Cause: The client disconnected before the streaming response completed, or the server encountered an error mid-stream.
Solution:
Handle connection errors gracefully:
Streaming responses require AsyncAPI schema definitions. Define your stream schema in your AsyncAPI spec and let Spikard manage the connection lifecycle. Implement graceful error handling in your event generator and monitor for client disconnects via timeout or network errors.
Prevention Tips:
- Implement proper error handling in generators
- Add heartbeat messages to detect dead connections
- Set appropriate timeout for streaming responses
- Test with flaky network conditions
Problem 20: Memory Leak with WebSocket Connections¶
Error Message:
MemoryError: Unable to allocate memory for WebSocket buffers.
Currently tracking 10000 active connections.
Root Cause: WebSocket connections aren't properly closed, leading to accumulation of stale connections and memory exhaustion.
Solution:
Implement connection lifecycle management:
WebSocket support is managed through AsyncAPI specifications. Define your WebSocket channels in your AsyncAPI schema and let Spikard handle connection lifecycle, timeouts, and resource cleanup automatically. Monitor your deployment metrics to catch memory growth early:
- Track active connection counts
- Set connection timeouts and limits
- Implement periodic health checks
- Monitor process memory and connection pool usage
Prevention Tips:
- Set connection limits
- Implement heartbeat/ping-pong to detect dead connections
- Add connection timeout
- Monitor active connection count
- Close connections on server shutdown
gRPC-Specific Issues¶
Problem 21: Protobuf Serialization Error¶
Error Message:
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.INVALID_ARGUMENT
details = "Failed to parse request proto: invalid wire type"
Root Cause: The client is sending a protobuf message that doesn't match the expected schema, often due to version mismatch between client and server.
Solution:
- Ensure client and server use same
.protofiles:
- Verify message creation:
# Python
import user_pb2
# INCORRECT - missing required fields
request = user_pb2.GetUserRequest()
# CORRECT
request = user_pb2.GetUserRequest(id="user_123")
# Or use dict syntax
request = user_pb2.GetUserRequest(**{"id": "user_123"})
- Check for breaking changes in
.proto:
// OLD
message UserRequest {
string id = 1;
}
// NEW - breaking change (field number changed)
message UserRequest {
string user_id = 1; // Changed field name AND number
}
// CORRECT - non-breaking change (field renamed but number kept)
message UserRequest {
string user_id = 1; // Renamed but kept field number
}
Prevention Tips:
- Version your
.protofiles - Never change field numbers
- Use reserved fields for deleted fields
- Test serialization round-trips
- Keep
.protofiles in a shared repository
Problem 22: gRPC Status Code UNAVAILABLE¶
Error Message:
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.UNAVAILABLE
details = "failed to connect to all addresses"
Root Cause: The gRPC server is not running, the address is incorrect, or there's a network connectivity issue.
Solution:
- Verify server is running:
- Check connection configuration:
# Python
import grpc
from user_pb2_grpc import UserServiceStub
# INCORRECT - wrong port
channel = grpc.insecure_channel('localhost:50050')
# CORRECT
channel = grpc.insecure_channel('localhost:50051')
stub = UserServiceStub(channel)
- Add retry logic:
# Python
import grpc
from grpc import StatusCode
def call_with_retry(stub, request, max_retries=3):
for attempt in range(max_retries):
try:
return stub.GetUser(request)
except grpc.RpcError as e:
if e.code() == StatusCode.UNAVAILABLE:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
raise
Prevention Tips:
- Use health check endpoints
- Implement connection pooling
- Add circuit breakers for resilience
- Monitor gRPC server metrics
- Use service discovery in production
Problem 23: gRPC Metadata Not Propagated¶
Error Message:
Root Cause: gRPC metadata (headers) aren't being properly set or accessed on the client or server side.
Solution:
- Set metadata on client:
# Python
import grpc
channel = grpc.insecure_channel('localhost:50051')
stub = UserServiceStub(channel)
# Add metadata
metadata = [
('authorization', 'Bearer token123'),
('x-request-id', 'req_456')
]
response = stub.GetUser(request, metadata=metadata)
# Ruby
require 'spikard/grpc'
request = Example::GetUserRequest.new(id: 'user_123')
metadata = {
'authorization' => 'Bearer token123',
'x-request-id' => 'req_456'
}
response = stub.get_user(request, metadata: metadata)
- Access metadata on server:
# Python
class UserService:
def GetUser(self, request, context):
# Access metadata
metadata = dict(context.invocation_metadata())
auth_token = metadata.get('authorization')
if not auth_token:
context.abort(grpc.StatusCode.UNAUTHENTICATED,
'Missing authorization header')
return user_pb2.User(id=request.id, name="John")
# Ruby
class UserServiceHandler < Spikard::Grpc::Handler
def handle_request(request)
auth_token = request.metadata['authorization']
unless auth_token
return Spikard::Grpc::Response.error(
'Missing authorization header'
)
end
# Process request
user = Example::User.new(id: request.payload.id, name: 'John')
Spikard::Grpc::Response.new(payload: Example::User.encode(user))
end
end
Prevention Tips:
- Document required metadata fields
- Validate metadata early in handlers
- Use interceptors for common metadata handling
- Log metadata for debugging
Problem 24: gRPC Streaming Deadlock¶
Error Message:
Root Cause: Bidirectional streaming has improper flow control, causing both sides to wait for each other.
Solution:
Implement proper streaming patterns:
# Python - INCORRECT (deadlock)
def bidi_stream(request_iterator):
responses = []
# Trying to read all requests before sending responses
for request in request_iterator:
responses.append(process(request))
# Never reaches here because client is waiting for response
for response in responses:
yield response
# CORRECT - interleaved reading and writing
def bidi_stream(request_iterator):
for request in request_iterator:
response = process(request)
yield response # Send immediately
// TypeScript - bidirectional streaming
async function* bidiStream(requests: AsyncIterable<Request>): AsyncIterable<Response> {
for await (const request of requests) {
const response = await processRequest(request);
yield response; // Stream responses as they're ready
}
}
Prevention Tips:
- Use unary or server/client streaming when possible
- Implement timeouts for streaming calls
- Test streaming with various message patterns
- Document expected message flow
Problem 25: gRPC Large Message Error¶
Error Message:
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
status = StatusCode.RESOURCE_EXHAUSTED
details = "Received message larger than max (4194304 vs. 4194304)"
Root Cause: The gRPC message exceeds the default 4MB size limit.
Solution:
- Increase message size limits:
# Python - Server
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
options=[
('grpc.max_receive_message_length', 50 * 1024 * 1024), # 50MB
('grpc.max_send_message_length', 50 * 1024 * 1024),
]
)
# Python - Client
channel = grpc.insecure_channel(
'localhost:50051',
options=[
('grpc.max_receive_message_length', 50 * 1024 * 1024),
('grpc.max_send_message_length', 50 * 1024 * 1024),
]
)
- Or use streaming for large data:
// Instead of single large message
service FileService {
rpc Upload(FileUploadRequest) returns (FileUploadResponse);
}
// Use streaming
service FileService {
rpc Upload(stream FileChunk) returns (FileUploadResponse);
}
message FileChunk {
bytes content = 1;
int64 offset = 2;
}
Prevention Tips:
- Use streaming for large payloads
- Implement chunking for file transfers
- Set appropriate size limits
- Monitor message sizes in production
- Consider alternative protocols for very large data
General Debugging Tips¶
Enable Debug Logging¶
# Python
import logging
logging.basicConfig(level=logging.DEBUG)
app = App()
app.config.debug = True
# Ruby
require 'logger'
app = Spikard::App.new
app.config.logger = Logger.new(STDOUT, level: :debug)
Validate Schemas¶
# AsyncAPI
spikard validate-asyncapi asyncapi.yaml
# Protobuf syntax
protoc --descriptor_set_out=/dev/null user.proto
Test Generated Code¶
# Python
pyrefly check gen/
# TypeScript
tsc --noEmit
# Ruby
steep check
# PHP
phpstan analyse --level=max
# Rust
cargo clippy -- -D warnings
Check Version Compatibility¶
Getting Help¶
If you encounter an issue not covered in this guide:
- Check the Spikard documentation
- Search GitHub issues
- Review examples directory
- Ask in the community Discord
- Report bugs with minimal reproduction case
When reporting issues, include:
- Spikard version (
spikard --version) - Language and runtime version
- Complete error message
- Minimal schema that reproduces the issue
- Steps to reproduce