bbwebservice
bbwebservice is a lightweight Python library for building small webservers. It keeps the original simplicity but now ships with multi-endpoint support, chunked request handling, scoped routing and an isolated multi-process / multi-thread worker pool. The main accept loop pushes sockets into a bounded queue while worker processes host thread pools that enforce watchdogs and keep Keep-Alive sessions responsive.
Quick Start
Spin up a minimal HTTPS-ready server in just a few lines:
from bbwebservice.webserver import register, MIME_TYPE
from bbwebservice import core
@register(route='::/hello', type=MIME_TYPE.TEXT)
def hello():
return 'Hello from bbwebservice!'
if __name__ == '__main__':
core.start()
Place this script next to a config/config.json (auto-created on first run) and
visit http://localhost:5000/hello. Enable TLS by pointing the config to a
certificate/key pair—no application changes needed. TLS contexts are prepared once per worker and reused (including SNI entries), so handshakes stay fast and certificate hot reloads propagate automatically.
Runtime model
- Main process accepts sockets, adds them to a connection queue (
connection_queue_size) and enforcesconnection_queue_timeoutbefore returning 503. It never blocks on worker backpressure, so SYN floods get absorbed in the queue instead of by the kernel. - Worker processes (
worker_processes) spin up thread pools (max_threads_per_process) and draw sockets from the queue. Each thread handles one client at a time but can process multiple sequential requests over a Keep-Alive connection. - Backpressure and limits:
max_threadscaps total active requests per server, while the queue makes short bursts resilient. Watchdogs enforcehandler_timeoutand send 504 responses if a handler stalls, without crashing the worker. - Streaming: helpers such as
PartialContentsanitize file paths, clamp byte ranges, and stream data in bounded chunks, preventing traversal bugs or memory blow-ups.
Runtime modes
bbwebservice now supports two runtime entrypoints:
runtime_mode=socket(default): start internal listeners viacore.start().runtime_mode=wsgi: run through an external WSGI server and importbbwebservice.wsgi:application.
Set runtime mode in config/config.json ("runtime_mode": "socket" | "wsgi") or override it with BBWS_RUNTIME_MODE.
If runtime_mode=wsgi, calling core.start() / webserver.start() raises:
RuntimeError: runtime_mode=wsgi disables core.start(); run via a WSGI server and import bbwebservice.wsgi:application
WSGI startup example (gunicorn)
BBWS_RUNTIME_MODE=wsgi gunicorn -w 4 -b 0.0.0.0:5000 bbwebservice.wsgi:application
Migration notes
- Existing socket users: no change required (
runtime_modedefaults tosocket). - WSGI adopters: keep your route decorators unchanged and switch only startup/runtime orchestration.
- Connection-level tuning (keep-alive policy, worker model, request timeouts at transport level) should be configured in your WSGI server/reverse proxy, not via
core.start(). - Current parity evidence and documented transport differences are tracked in
docs/wsgi_parity_checklist.md. - Rollout/canary/rollback guidance is tracked in
docs/wsgi_release_rollout.md.
Installation
pip install bbwebservice
Usage
- import helpers:
from bbwebservice.webserver import *
from bbwebservice import core
1. Register pages for HTTP GET
@register(route=..., type=...)registers a handler for a GET route.routeaccepts either a plain string or the selector syntaxip:port::domain:/path. Examples:'::/status'– matches every endpoint'127.0.0.1::/debug'– IPv4 127.0.0.1 on any port and any domain':::example.com:/info'– any IP/port, only domainexample.comUrlTemplate('[::1]:8000::/v1/{slug:str}')– IPv6 with typed placeholders
typespecifies the MIME type of the response.
@register(route='::/hello', type=MIME_TYPE.TEXT)
def hello():
return 'Hello World'
2. Register pages for HTTP POST
@post_handlerworks like@registerbut the decorated function must accept anargsparameter.
@post_handler(route='::/login', type=MIME_TYPE.JSON)
def login(args):
payload = args[STORE_VARS.POST].decode('utf-8')
return {'status': 'ok', 'raw': payload}
3. Register handlers for other HTTP verbs
- Additional decorators mirror
@post_handler:@put_handler(...)@patch_handler(...)@delete_handler(...)@options_handler(...)
- They share the same selector syntax and MIME type handling.
OPTIONShandlers may omit theargsparameter if you only need static responses;HEADautomatically reuses the correspondingGEThandler and suppresses the body.
@put_handler(route='::/items/{slug:str}', type=MIME_TYPE.JSON)
def update_item(args):
data = json.loads(args[STORE_VARS.POST].decode('utf-8'))
return {'slug': args[STORE_VARS.TEMPLATE_VARS]['slug'], 'data': data}
@options_handler(route='::/items/*', type=MIME_TYPE.TEXT)
def describe_items():
return 'Allowed: GET, POST, PUT, DELETE, PATCH'
4. Redirects
- Return a
Redirectobject to send 303/307 style responses.
@register(route='::/old', type=MIME_TYPE.HTML)
def legacy():
return Redirect('/new')
5. Partial content / streaming
- Use
PartialContentfor ranged responses (video, downloads, etc.). Paths are normalized to stay within your content directory and byte ranges are clamped/streamed to prevent traversal or memory issues.
@register(route='::/video', type=MIME_TYPE.MP4)
def video(_):
return PartialContent('/content/movie.mp4', default_size=80_000)
6. Error handler
@error_handler(error_code=..., type=...)provides fallback pages.
@error_handler(error_code=404, type=MIME_TYPE.HTML)
def not_found():
return load_file('/content/404.html')
7. Handler arguments
- Handlers that accept args can read or modify cookies, headers, query strings, etc.
@register(route='::/inspect', type=MIME_TYPE.JSON)
def inspect(args):
args['response'].header.add_header_line(
Header_Line(Response_Header_Tag.SERVER, 'bbwebservice')
)
return args
8. Start the server
- Use
core.start()to launch all configured listeners.
@register(route='::/index', type=MIME_TYPE.HTML)
def index():
return load_file('/content/index.html')
core.start()
9. URL templates
- Dynamic routes use
UrlTemplatewith typed placeholders.
| Supported Types | Example |
|---|---|
str |
{name:str} |
int |
{id:int} |
float |
{value:float} |
bool |
{flag:bool} |
path |
{path:path} |
Notes:
- Placeholders must be separated by literal characters inside a segment (e.g.,
file-{id:int}.jsonworks, but{a:int}{b:int}is rejected because it is ambiguous). {path:path}can appear at most once per template, must represent an entire segment, and must be the final segment so it can safely capture the remainder of the path.
@register(route=UrlTemplate('::/user/{name:str}/{age:int}'), type=MIME_TYPE.JSON)
def user(args):
return args[STORE_VARS.TEMPLATE_VARS]
10. Selector hierarchy
- The most specific selector wins automatically (IP > port > domain > global). Register routes knowing that concrete bindings take precedence over generic ones.
11. Response helpers
- In addition to returning bytes/strings, you can respond with:
Dynamic(content, mime_type)– content with a custom MIME typeStreamResponse(chunks, mime_type=..., status=..., headers=...)– chunked streaming without bufferingSseEvent(data, event=None).to_bytes()– formatsdata:/event:lines for SSE streamsPartialContent/RedirectResponse(...)– convenience for setting status, headers, body in one object
11.1 Streaming responses (chunked + SSE)
- Return
StreamResponsewhen you want to send data as it is produced. Each chunk is framed as HTTP/1.1 chunked encoding and flushed; noContent-Lengthis set. - Chunks can be
bytesorstr, from iterables or async iterables. - SSE convenience: set
mime_type="text/event-stream"and yieldSseEvent(...).to_bytes(). Compression is disabled and events flush immediately. - Safety limits (configurable per server or globally):
stream_max_chunk_size,stream_max_total_bytes,stream_max_duration(seconds),stream_idle_timeout(seconds). Streams are aborted cleanly if limits are exceeded.
@post_handler(route="::/api/chat/stream", type=MIME_TYPE.JSON)
def chat_stream(args):
def gen():
yield b'{"message":"start"}\n'
for chunk in token_stream():
yield chunk # bytes or str
return StreamResponse(gen(), mime_type="application/json")
@register(route="::/events", type=MIME_TYPE.TEXT)
def events(_):
stream = (SseEvent(data=json.dumps(t), event="update").to_bytes() for t in token_stream())
return StreamResponse(stream, mime_type="text/event-stream", headers=[("Cache-Control", "no-cache")])
11.2 Dynamic MIME selection
- Use
type=MIME_TYPE.DYNAMICon a route to defer theContent-Typeto the returnedResponse/StreamResponse/Dynamicobject. This is useful when the handler decides the MIME at runtime.
12. Background tasks
server_task(func, interval)schedules functions (receives global state ifdataparameter present). Tasks shut down gracefully with the server.
13. CORS
- Enable or disable at runtime:
webserver.enable_cors(
allow_origin="*",
allow_methods=["GET", "POST"],
allow_headers=["Content-Type"],
expose_headers=["X-Total-Count"],
allow_credentials=False,
max_age=600,
)
disable_cors() and get_cors_settings() are provided as well. OPTIONS requests are answered automatically when CORS is active.
14. Worker processes and backpressure
- The main process accepts sockets and hands them to a pool of worker processes (size controlled by
worker_processes, defaulting to your CPU count). Each worker can serve multiple requests sequentially and enforceshandler_timeoutlocally; if a handler overruns, the worker sends a 504 response and is recycled. - Per-listener
max_threadsplus the globalmax_threadsstill provide backpressure: they cap how many sockets may be in flight concurrently. When the pool is saturated, new connections get503 Service Unavailable, preventing unbounded queuing.
15. Request parsing
- Headers are read incrementally up to
max_header_size, bodies honourContent-LengthorTransfer-Encoding: chunked(trailers are skipped). Chunked data is decoded into theargs[STORE_VARS.POST]buffer. Oversized requests trigger 413/431 responses, andmax_url_lengthrejects pathological request targets before they reach your handlers.
16. Logging
- Logging honours scopes (
ip:port::domain) and only formats messages when a sink is active.
set_logging(LOGGING_OPTIONS.INFO, True)
log_to_file('/logs/server.log', [LOGGING_OPTIONS.ERROR, LOGGING_OPTIONS.INFO])
set_logging_callback(lambda msg, ts, lvl: print('[callback]', lvl, msg))
There is also webserver.response() for building structured responses and log() for manual logging with scopes.
Server Configuration
config/config.json controls listeners and limits:
{
"max_threads": 100,
"max_threads_per_process": 16,
"max_header_size": 16384,
"max_body_size": 10485760,
"keep_alive_timeout": 15,
"keep_alive_max_requests": 100,
"header_timeout": 10,
"body_min_rate_bytes_per_sec": 1024,
"handler_timeout": 30,
"stream_max_chunk_size": 65536,
"stream_max_total_bytes": 10485760,
"stream_max_duration": 60.0,
"stream_idle_timeout": 10.0,
"connection_queue_timeout": 2,
"ssl_handshake_timeout": 5,
"max_url_length": 2048,
"worker_processes": 4,
"worker_timeout_threshold": 0,
"server": [
{
"ip": "default",
"port": 5000,
"queue_size": 32,
"max_threads": 25,
"max_threads_per_process": 16,
"SSL": false,
"host": "",
"cert_path": "",
"key_path": "",
"https-redirect": false,
"https-redirect-escape-paths": [],
"update-cert-state": false,
"stream_max_chunk_size": 65536,
"stream_max_total_bytes": 10485760,
"stream_max_duration": 60.0,
"stream_idle_timeout": 10.0
}
]
}
ip:defaultresolves at runtime, otherwise explicit IPv4/IPv6 (use[::1]style).- Multiple entries in
serverbind additional sockets. SSLwithhostas list enables SNI (each entry supplieshost,cert_path,key_path). Failed certificates are logged with full paths.https-redirectforces 301 to HTTPS except for paths listed inhttps-redirect-escape-paths(supports wildcard suffix*).update-cert-statewatches certificate files and reloads them automatically.keep_alive_timeout/keep_alive_max_requestscontrol idle keep-alive budgeting.max_threads_per_processdefines how many threads each worker process spawns (total parallel capacity is roughlyworker_processes * max_threads_per_process).header_timeoutcaps how long headers may stream in (protects against Slowloris).- Streaming limits:
stream_max_chunk_size(bytes per frame),stream_max_total_bytes(total payload),stream_max_duration(seconds overall),stream_idle_timeout(seconds between chunks). Defaults apply globally and can be overridden per server entry. body_min_rate_bytes_per_secenforces a minimum upload rate and returns 408 if the client stalls.handler_timeoutwraps user handlers and turns overruns into 504 Gateway Timeout responses (the offending thread closes the socket; workers recycle themselves only when too many timeouts occur).connection_queue_timeoutlimits how long an accepted socket may wait in the internal dispatcher queue before we proactively respond with HTTP 503 (set to0to wait indefinitely).ssl_handshake_timeoutcloses TLS clients that never finish the handshake.max_url_lengthdrops requests with excessively long targets before routing begins.worker_processessets how many handler processes run in parallel (default: CPU count). Together withmax_threads_per_processit determines the theoretical maximum concurrency, whilemax_threadslets you set a hard per-server cap below that maximum.worker_timeout_thresholdcontrols how many handler watchdog timeouts a worker tolerates before draining/restarting. Values< 1are treated as a ratio ofmax_threads_per_process(default0.5, i.e., 50% of the thread pool); values>= 1are treated as absolute counts. All of these timeout values may also be specified on each individualserverentry to override the global defaults for just that listener.
Recommended ports: 5000 (local), 80 (HTTP), 443/8443 (HTTPS).
Logging recap
set_logging(LOGGING_OPTIONS.DEBUG, True)
set_logging(LOGGING_OPTIONS.TIME, True)
set_logging(LOGGING_OPTIONS.TIMEOUT, True)
log_to_file()
Use set_logging(scope='127.0.0.1:5000::example.com', ...) to target specific endpoints.
Metrics
Timeout events are tallied per category (header, body, handler) and exposed through the built-in endpoint /_bbws/metrics/timeouts. Each request returns JSON such as {"timeouts.header": 1, "timeouts.body": 0, "timeouts.handler": 2} so monitoring systems can alert on abusive clients or stuck handlers. Use webserver.expose_timeout_metrics('::/custom/path') if you need to relocate the endpoint.
License
MIT License © Lukas Walker (see LICENSE for details).
Metadata
Release files for bbwebservice 3.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bbwebservice-3.2.3.tar.gz | 74.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bbwebservice-3.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 147.9 kB
Release files / bbwebservice-3.2.3.tar.gz
| Download URL | bbwebservice-3.2.3.tar.gz |
|---|---|
| Size | 74.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c58dccf3991eaf2ce0675665a8309d90f11056a9e59909f8538f524e12bb679
|
|
BLAKE2b-256 checksum How to use checksums |
20e20f34d3a9b2a7bfbb623ce358ff780420658c971a38e12a771876bb40df58
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / bbwebservice-3.2.3-py3-none-any.whl
| Download URL | bbwebservice-3.2.3-py3-none-any.whl |
|---|---|
| Size | 73.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
52880d1f3c010d7ae4ced908185ceb7628d975d1a232a1a123d5cfdfa45b8301
|
|
BLAKE2b-256 checksum How to use checksums |
c555b390244f979a05e843d2a7f19ae0dfab87deb870fa8425e38fb10472facc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|