BedSheetUser Guide
Overview
BedSheet is a Fantom framework for delivering web applications.
Built on top of IoC and Wisp, BedSheet provides a rich middleware mechanism for the routing and delivery of content over HTTP.
BedSheet is inspired by Java's Tapestry5, Ruby's Sinatra and Fantom's Draft.
Install
Install BedSheet with the Fantom Repository Manager ( fanr ):
C:\> fanr install -r http://repo.status302.com/fanr/ afBedSheet
To use in a Fantom project, add a dependency to build.fan:
depends = ["sys 1.0", ..., "afBedSheet 1.3+"]
Documentation
Full API & fandocs are available on the Status302 repository.
Quick Start
1). Create a text file called Example.fan:
using afIoc
using afBedSheet
class HelloPage {
Text hello(Str name, Int iq := 666) {
return Text.fromPlain("Hello! I'm $name and I have an IQ of $iq!")
}
}
class AppModule {
@Contribute { serviceType=Routes# }
static Void contributeRoutes(Configuration conf) {
conf.add(Route(`/index`, Text.fromPlain("Welcome to BedSheet!")))
conf.add(Route(`/hello/**`, HelloPage#hello))
}
}
class Example {
Int main() {
afBedSheet::Main().main([AppModule#.qname, "8080"])
}
}
2). Run Example.fan as a Fantom script from the command line:
C:\> fan Example.fan -env development ... BedSheet v1.2 started up in 323ms C:\> curl http://localhost:8080/index Welcome to BedSheet! C:\> curl http://localhost:8080/hello/Traci/69 Hello! I'm Traci and I have an IQ of 69! C:\> curl http://localhost:8080/hello/Luci Hello! I'm Luci and I have an IQ of 666!
Wow! That's awesome! But what just happened!?
Every BedSheet application has an AppModule that configures IoC services. Here we told the Routes service to return some plain text in response to /index and to call the HelloPage#hello method for all requests that start with /hello. Route converts URI path segments into method arguments, or in our case, to Str name and to an optional Int iq.
Route handlers are typically what we, the application developers, write. They perform logic processing and render responses. Our HelloPage route handler simply returns a plain Text response, which BedSheet sends to the client via an appropriate ResponseProcessor.
Starting BedSheet
Every Bed App (BedSheet Application) has an AppModule class that defines and configures your IoC services. It is an IoC concept that allows you centralise your application's configuration in one place. It is the AppModule that defines your Bed App and is central everything it does.
To start BedSheet from the command line, you need to tell it where to find the AppModule and which port to run on:
C:\> fan afBedSheet -env development <fully-qualified-app-module-name> <port-number>
For example:
C:\> fan afBedSheet -env development myWebApp::AppModule 8069
TIP: Should your AppModule grow too big, break logical chunks out into their own classes using the @SubModule facet.
<fully-qualified-app-module-name> may be replaced with just <pod-name> as long as your pod's build.fan defines the following meta:
meta = [
...
...
"afIoc.module" : "<fully-qualified-app-module-name>"
]
This allows BedSheet to look up your AppModule from the pod. Example:
C:\> fan afBedSheet -env development myWebApp 8069
Note that AppModule is named so out of convention but the class may be called anything you like.
Request Routing
The Routes service maps HTTP request URIs to response objects and handler methods. It is where you would typically define how requests are handled. You configure the Routes service by contributing instances of Route. Example:
using afIoc
using afBedSheet
class AppModule {
@Contribute { serviceType=Routes# }
static Void contributeRoutes(Configuration conf) {
conf.add(Route(`/home`, Redirect.movedTemporarily(`/index`)))
conf.add(Route(`/index`, IndexPage#service))
}
}
Route objects take a matching glob and a response object. A response object is any object that BedSheet knows how to process or a Method to be called. If a method is given, then request URI path segments are matched to the method parameters. See Route for more details.
Draft Routes
If you prefer the draft style of routing, that's no problem, you can use Draft Routes in BedSheet!
Add BedSheet Draft and draft as dependencies in your build.fan and you can contribute Draft Route objects to the Routes service.
Routing lesson over.
(...you Aussies may stop giggling now.)
Route Handling
Route Handler is the name given to a method that is called by a Route. They process logic and generally don't pipe anything to the HTTP response stream. Instead they return a Response Object for further processing. For example, the Quick Start HelloPage route handler returns a Text object.
Route handlers are usually written by the application developer, but a couple of common use-cases are bundled with BedSheet:
- FileHandler: Maps request URIs to files on file system.
- PodHandler : Maps request URIs to pod file resources.
Response Objects
Response Objects are returned from Route Handlers. It is then the job of Response Processors to process these objects, converting them into data to be sent to the client. Response Processors may themselves return a Response Object, which will be handled by another Response Processor.
You can define Response Processors and process Response Objects yourself; but by default, BedSheet handles the following:
Void/null/false: Processing should fall through to the next Route match.true: No further processing is required.- File : The file is streamed to the client.
- HttpStatus : Sets the HTTP response status and renders a mini html page. (See HTTP Status Processing.)
- InStream : The
InStreamis piped to the client. TheInStreamis guarenteed to be closed. - MethodCall : The method is called and the return value used for further processing.
- Redirect : Sends a 3xx redirect response to the client.
- Text : The text (be it plain, json, xml, etc...) is sent to the client with a corresponding
Content-Type.
Template Rendering
Templating, or formatting text (HTML or otherwise) is left for other 3rd party libraries and is not a conern of BedSheet. That said, there a couple templating libraries out there and integrating them into BedSheet is relatively simple. For instance, Alien-Factory provides the following libraries:
- Slim for rendering HTML,
- Pillow for integrating efanXtra components (may be used with Slim!),
- BedSheet Efan for basic efan (Embedded Fantom) integration, and
- BedSheetMoustache for integrating Mustache templates.
Taking Slim as an example, simply inject the service in your Route Handler and use it to return a Text object:
using afIoc
using afBedSheet
using afSlim
class IndexPage {
@Inject Slim? slim
Text render() {
xhtml := slim.renderFromFile(`xmas.xhtml.slim`.toFile)
return Text.fromXhtml(xhtml)
}
}
BedSheet Middleware
When a HTTP request is received, it is passed through a pipeline of BedSheet Middleware; this is a similar to Java Servlet Filters. If the request reaches the end of the pipeline without being processed, a 404 is returned.
Middleware bundled with BedSheet include:
Routes: Performs the standard request routingRequestLog: Generates request logs in the standard W3C Extended Log File Format.
You can define your own middleware to address cross cutting concerns such as authentication and authorisation. See the FantomFactory article Basic HTTP Authentication With BedSheet for working examples.
Error Processing
When BedSheet catches an Err it scans through a list of contributed ErrProcessors to find one that can handle the Err. ErrProcessors take an Err and return a Response Object for further processing (for example, Text). Or it may return true if the error has been completely handled and no further processing is required.
If no matching ErrProcessor is found then BedSheet displays its default Err500 page - which is extremely verbose, displays (a shed load of) debugging information and is highly customisable.

The default Err page is great for development! But not so great for production - stack traces tend to scare Joe Public. So note that in a production environment (see IocEnv) a simple HTTP status page is displayed instead.
ALIEN-AID: BedSheet defaults to production mode, so set an environment variable called
ENVwith the valuedevelopmentto ensure you continue to see the BedSheet's verbose Err500 page. See this Fantom-Factory article for more details.
To add a custom error page, contribute an ErrProcessor to ErrProcessors:
@Contribute { serviceType=ErrProcessors# }
static Void contributeErrProcessors(MappedConfig conf) {
conf[Err#] = conf.autobuild(MyErrHandler#)
}
HTTP Status Processing
HttpStatus responses are handled by HttpStatusProcessors which select a contributed processor dependent on the HTTP status code. If none are found, a default catch all processor sets the HTTP status code and sends a mini html page to the client. This is the default page you see when you receive a 404 Not Found error.

To set your own 404 Not Found page, contribute a HttpStatusProcessor to the HttpStatusProcessors service for the status code 404:
@Contribute { serviceType=HttpStatusProcessors# }
static Void contributeHttpStatusProcessors(MappedConfig conf) {
conf[404] = conf.autobuild(My404Handler#)
}
Config Injection
BedSheet uses IoC Config to give injectable @Config values. @Config values are essesntially a map of Str to immutable / constant values that may be set and overriden at application start up. (Consider config values to be immutable once the app has started).
BedSheet sets the initial config values by contributing to the FactoryDefaults service. An application may then override these values by contibuting to the ApplicationDefaults service.
@Contribute { serviceType=ApplicationDefaults# }
static Void contributeApplicationDefaults(MappedConfig conf) {
...
conf["afBedSheet.errPrinter.noOfStackFrames"] = 100
...
}
All BedSheet config keys are listed in BedSheetConfigIds meaning the above can be more safely rewriten as:
conf[BedSheetConfigIds.noOfStackFrames] = 100
To inject config values in your services, use the @Config facet with conjunction with IoC's @Inject:
@Inject @Config { id="afBedSheet.errPrinter.noOfStackFrames" }
Int noOfStackFrames
The config mechanism is not just for BedSheet, you can use it too when creating 3rd Party libraries! Contributing initial values to FactoryDefaults gives users of your library an easy way to override your values.
Request Logging
BedSheet can generate standard HTTP request logs in the W3C Extended Log File Format.
To enable, just configure the directory where the logs should be written and (optionally) set the log filename, or filename pattern for log rotation:
@Contribute { serviceType=ApplicationDefaults# }
static Void contributeApplicationDefaults(Configuration conf) {
conf[BedSheetConfigIds.requestLogDir] = `/my/log/dir/`
conf[BedSheetConfigIds.requestLogFilenamePattern] = "bedSheet-{YYYY-MM}.log"
}
Ensure the log dir ends in a trailing /slash/.
The fields writen to the logs may be set by configuring BedSheetConfigIds.requestLogFields, but default to looking like:
2013-02-22 13:13:13 127.0.0.1 - GET /doc - 200 222 "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) etc" "http://localhost/index"
Development Proxy
Never (manually) restart your app again!
Use the -proxy option when starting BedSheet to create a Development Proxy and your app will auto re-start when a pod is updated:
C:\> fan afBedSheet -proxy <mypod> <port>
The proxy sits on <port> and starts your real app on <port>+1, forwarding all requests to it.
Client <--> Proxy (port) <--> Web App (port+1)
A problem other (Fantom) web development proxies suffer from is that, when the proxy dies, your real web app is left hanging around; requiring you to manually kill it.
Client <--> ???????? <--> Web App (port+1)
BedSheet applications go a step further and, should it be started in proxy mode, it pings the proxy every second to stay alive. Should the proxy not respond, the web app kills itself.
See proxyPingInterval for more details.
Gzip
By default, BedSheet compresses HTTP responses with gzip where it can.(1) But it doesn't do this willy nilly, oh no! There are many hurdles to overcome...
Disable All
Gzip, although enabled by default, can be disabled for the entire web app by setting the following config property:
config[BedSheetConfigIds.gzipDisabled] = true
Disable per Response
Gzip can be disabled on a per request / response basis by calling the following:
httpResponse.disableGzip()
Gzip'able Mime Types
Not everything should be gzipped. For example, text files gzip very well and yield high compression rates. JPG images on the other hand, because they're already compressed, don't gzip well and can end up bigger than the original! For this reason you must contribute to the GzipCompressible service to enable gzip for specified Mime Types:
config["text/funky"] = true
(Note: The GzipCompressible contrib type is actually sys::MimeType - IoC kindly coerces the Str to MimeType for us.)
By default BedSheet will compress plain text, css, html, javascript, xml, json and other text responses.
Gzip only when asked
Guaranteed that someone, somewhere is still using Internet Explorer 3.0 and they can't handle gzipped content. As such, and as per RFC 2616 HTTP1.1 Sec14.3, we only gzip the response if the client actually asked for it!
Min content threshold
Gzip is great when compressing large files, but if you've only got a few bytes to squash... the compressed version is going to be bigger, which kinda defeats the point of using gzip in the first place! For that reason the response data must reach a minimum size / threshold before it gets gzipped.
See GzipOutStream and gzipThreshold for more details.
Phew! Made it!
If (and only if!) your request passed all the tests above, will it then be lovingly gzipped and sent to the client.
Buffered Response
By default, BedSheet attempts to set the Content-Length HTTP response header.(2) It does this by buffering HttpResponse.out. When the stream is closed, it writes the Content-Length and pipes the buffer to the real HTTP response.
Response buffering can be disabled on a per HTTP response basis.
A threshold can be set, whereby if the buffer exeeds that value, all content is streamed directly to the client.
See BufferedOutStream and responseBufferThreshold for more details.
Tips
All route handlers and processors are built by IoC so feel free to @Inject DAOs and other services.
BedSheet itself is built with IoC so look at the BedSheet Source for IoC examples.
Even if your route handlers aren't services, if they're const classes, they're cached by BedSheet and reused on every request.
Go Live with Heroku
In a hurry to go live? Use Heroku!
Heroku and the heroku-fantom-buildpack makes it ridiculously to deploy your web app to a live server. Just check in your code and Heroku will build your web app from source and deploy it to a live environment!
To have Heroku run your BedSheet web app you have 2 options:
1) Create a Heroku text file called Procfile at the same level as your build.fan with the following line:
web: fan afBedSheet <fully-qualified-app-module-name> $PORT
substituting <fully-qualified-app-module-name> with, err, your fully qualified app module name! Example, MyPod::AppModule. Type $PORT verbatim, as it is.
2) Create a Main class in your app:
using util
class Main : AbstractMain {
@Arg { help="The HTTP port to run the app on" }
private Int port
override Int run() {
return afBedSheet::Main().main("<fully-qualified-app-module-name> $port".split)
}
}
Main classes have the advantage of being easy to run from an IDE or cmd line.
See heroku-fantom-buildpack for more details.
Release Notes
v1.3.12
- New: Static files are served with a default
Cache-ControlHTTP response header, change via Ioc Config. - New: Added
BedSheetServerto replaceBedSheetMetaData, contains methods for generating absolute URLs. - New: Added
toClientUrl()andtoAbsoluteUrl()toBedSheetServer. - New: Added
HttpResponseHeaders.varyto responses that could be gzipped. - Chg: Updated to use IoC 1.7.2.
- Chg: Renamed
HttpRequest.modRel->HttpRequest.url.], deprecatedabsUri,modBase,modRelanduri. - Chg:
HttpSessionfails fast on attempts to store non-serialisable values. - Chg: Gzip responses also set a HTTP header of
Vary: Accept-Encoding. - Chg: Added the
Cache-ControlHTTP response header to error pages to ensure they're never cached. - Chg: Renamed
Text.fromMimeType()->Text.fromContentType(). - Chg:
HttpRequestHeaders.hostis now aStr.
v1.3.10
- New:
FileAssetobjects are generated by theFileHandlerservice and containclientUrlsfor your web page. - Chg:
ValueEncodersservice now letsReProcessErrspass through un-hindered. - Chg: Overhauled
FileHandlerAPI. (Breaking change) - Chg: Renamed
Text.mimeType->Text.contentType.
v1.3.8
- New: Set response headers
X-BedSheet-errMsg,X-BedSheet-errTypeandX-BedSheet-errStackTracewhen processing an Err. (Dev mode only) - New: Boring stack frames on the Err500 page are muted (greyed) and may be toggled on and off completely via a checkbox.
- New:
FileHandlernow responds toHEADrequests. - New: Introduced a
FileMetaCacheto prevent excessive hits on the file system. - Chg: Updated to use IoC 1.6.2 and Bean Utils.
- Chg: BedSheet pages (404, 500 & Welcome) are served up as XHTML.
- Chg:
ReProcessErrmay now re-process non-const response objects. - Bug:
FlashMiddlewareneedlessly created http sessions.
v1.3.6
- New: Added
ActorPoolssection to Err500 page. - Chg: Updated to use IoC 1.6.0 and Concurrent.
- Chg:
Routesnow perform some basic validation to catch cases where the Uri would never match the method handler. - Chg: Atom (RSS) feeds
application/atom+xmlare now GZip compressible. - Chg: Not found requests for HTTP methods other than GET or POST return a 501, not 404.
- Chg: All BedSheet pages render as valid XML.
- Bug: If 2
Routeshad the same Regex, only 1 was shown on the 404 page. - Bug:
HttpFlashdata could leak into concurrent web requests.
v1.3.4
- New: Added
fromClientUri()andfromServerFile()toFileHandler. - New:
IocConfigvalues, BedSheet Routes and Fantom Pods are now printed on the standard Err page. - Chg: Added some handy
toStrmethods toRouteand response objects. - Chg: Pretty printed the Str maps that get logged on Err.
v1.3.2
- New: Added
appNametoBedSheetMetaDatathat returns theproj.namefrom the application's pod meta. - Chg: Added
matchesMethod()andmatchesParams()helper methods toRouteResponseFactory. - Chg: Made
ErrPrinterStrandErrPrinterHtmlpublic, but@NoDoc, as they're useful for emails et al. - Chg: Made
HttpRequestHeadersandHttpResponsetHeadersconst classes, backed byWebReqandWebRes. - Bug: Ensured
HttpRequest.modRelalways returns a path absolute uri - see Inconsistent WebReq::modRel() - Bug: Application could NPE on startup if an
AppModulecould not be found.
v1.3.0
- New: Added
HttpCookiesservice, removed corresponding cookie methods fromHttpRequestandHttpResponse. (Breaking change) - New: Added
stash()toHttpRequest - New: Added
fromXhtml(...)toText - New: Added
contentLength()andcookie()toHttpRequestHeaders - New:
MethodCallResponseProcessoruses IoC to call methods so that it may inject any dependencies / services as method arguments. - New: Added
StackFrameFilterto filter out lines in stack traces. - New: Added
hosttoBedSheetConfigIds, mainly for use by 3rd party libraries. - Chg: Upgraded to IoC 1.5.2.
- Chg: Removed
BedServerandBedClient, they have been moved to Bounce. (Breaking change) - Chg: Removed
@Config, use@afIocConfig::Configinstead. (Breaking change) - Chg: Renamed
HttpPipelineFilter->Middlewareand updated the corresponding services. Hardcoded the default BedSheet filters / middleware to the start of the pipeline. (Breaking change) - Chg: Renamed
HttpRequestLogFilter->RequestLogMiddlewareand updated the@Configvalues. (Breaking change) - Chg:
@NoDoced some services as they're only referenced by@Contributemethods:ErrProcessors, HttpStatusProcessors, ResponseProcessor, ValueEncoder. - Chg:
QualityValuesare nullable fromHttpRequestHeaders
v1.2.4
- Chg: Upgraded to IoC 1.5.0.
- Chg: Upgraded to IoC Config 1.0.0.
v1.2.2
- New: Added
gzipcompression for web fonts. - New: BedSheet connection details printed on startup.
- Chg:
FileHandlernow lets non-existant files fall through. - Chg:
FileHandlerauto addsRoutemappings to theRoutesservice. - Chg: Added more info to the BedSheet 404 page in dev.
- Chg: Gave more control over the verbose rendering of the standard
BedSheetpages. - Bug:
BedServergenerated the wrong info forBedSheetMetaData- required when testing Pillow web apps.
v1.2.0
- New: Route objects may take any response result - not just
Methods! - New: BedSheet now has a dependency on IoC Env
- Chg:
HttpRequestLogFilteris now in the Http Pipeline by default - it just needs enabling. - Chg: The detailed BedSheet Err500 page is disabled in
productionenvironments. - Chg: Rejigged how the default
ErrProcessoris used, making it easier to plug in your own. (Breaking change.) - Chg:
BedSheetConfigIdsrenamed fromConfigIds. (Breaking change.) - Chg: Removed Route Matching -
Routesnow only takeRouteobjects. (Breaking change.) - Chg: Removed
IeAjaxCacheBustingFilterwith no replacement. (Breaking change.) - Chg: Removed
CorsHandlerwith no replacement. (Breaking change.) - Chg: Massaged a lot of the documentation.
v1.1.4
- New: The cause of startup Errs are printed before service shutdown - see this topic.
- Chg: Better Err msg if
AppModuletype not found on startup. - Chg: Disabled afIoc service list on startup.
- Bug:
BedServerwould crash if the app requiredBedSheetMetaData.
v1.1.2
- New: Added
Causessection to Err500 page. - Chg: Faster startup times when using a proxy
- Chg: Better Err handling on app startup
- Bug: Transitive dependencies have been re-instated.
- Bug: The
-noTransDepsstartup option now propogates through the proxy.
v1.1.0
- New: Added
BedSheetMetaDatawith information on whichAppModuleafbedSheet was started with. - Chg: Renamed
RouteHandler->MethodInvoker. (Breaking change.) - Chg: Injectable services are now documented with
(Service). - Chg: Moved internal proxy options in
Mainto their own class. - Chg: Enabled multi-line quotes.
- Bug:
IoC Configwas not always added as a transitive dependency. (Thanks toLightDyefor reporting.)
v1.0.16
- New: Added
Available Valuessection to Err500 page, fromafIoc::NotFoundErr. - Chg: Broke
@Configcode out into its own module: IoC Config. - Chg: Added a skull logo to the
Err500page. - Chg: Rejigged the
Err500section layout and tweaked the source code styling.
v1.0.14
- New:
SrcCodeErrsfrom afPlastic / efan are printed in the default Err500 pages. - New: Added
ConfigSource.getCoerced()method. - New: Added Template Rendering to fandoc.
v1.0.12
- New: Added
IoC Operation Tracesection to Err500 page. - New: Added
Moustache Compilation Errsection to Err500 page. - Chg: Moved Moustache out into it's own project.
- Chg: Anyone may now contribute sections to the default verbose Err500 page.
- Bug: Module name was not always found correctly on startup.
v1.0.10
- Bug: This documentation page didn't render.
v1.0.8
- Chg: Updated to use
afIoc-1.4.x - Chg: Overhauled Route to match
nullvalues. Thanks go to LightDye. - Chg: Warnings on startup if an AppModule could not be found - see Issue #1. Thanks go to Jorge Ortiz.
- Chg: Better Err handling when a dir is not mapped to
FileHandler - Chg: Transferred VCS ownership to AlienFactory
- Chg: Test code is no longer distributed with the afBedSheet.pod.
v1.0.6
- Chg:
HttpResponse.statusCodeis now a field. - Chg:
HttpResponse.disableGzipis now a field. - Chg:
HttpResponse.disableBufferingis now a field.
v1.0.4
- New: Added
BedServerandBedClientto test BedSheet apps without using a real web server.
v1.0.2
- New: Initial release