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 Respository Manager:
C:\> fanr install -r http://repo.status302.com/fanr/ afBedSheet
Or to install manually, download the pod from Status302 and copy it to %FAN_HOME%/lib/fan/
.
To use in a Fantom project, add a dependency to its build.fan
:
depends = ["sys 1.0", ..., "afBedSheet 1.2+"]
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(OrderedConfig 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
.
Request handlers are typically what we, the app developers, write. They perform logic processing and render responses. Our HelloPage
request handler simply returns a plain Text response.
A default ResponseProcessor then sends the Text response to the client.
Starting BedSheet
BedSheet
applications may be started by invoking BedSheet
from the command line:
C:\> fan afBedSheet <fully-qualified-app-module-name> <port-number>
For example:
C:\> fan afBedSheet myWebApp::AppModule 80
Every BedSheet
application has an AppModule
class that defines and configures your Ioc services. It is an Ioc concept that allows you centralise your application configuration in one place.
TIP: Should your AppModule grow too big, break logical chunks out into their own classes using the @SubModule facet.
You may find it convenient to create your own BedSheet
Main wrapper.
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) } }
<fully-qualified-app-module-name>
may be replaced with <pod-name>
as long as your pod's build.fan
defines the following meta:
meta = [ ... "afIoc.module" : "<fully-qualified-app-module-name>", ... ]
Note that AppModule
is named so out of convention but may be called anything you like.
HTTP Pipeline
When a http request is received it is passed through a pipeline of filters. The terminating filter performs the standard BedSheet
routing (see Request Routing). Filters can be used to address cross cutting concerns such as authorisation.
Standard BedSheet filters are:
HttpCleanupFilter
: Ensures theHttpOutStream
is closed and cleans up all request / thread scoped data.HttpErrFilter
: Catches and processes Errs, see Error Processing.HttpFlashFilter
: Stores values from one http request to the next.- HttpRequestLogFilter: Generates request logs in the standard W3C Extended Log File Format. (Not enabled by default.)
It is important that these are first in the pipeline. As such, when contributing your own filters you are encouraged to add the ordering constraint of "after: BedSheetFilters"
, example:
@Contribute { serviceType=HttpPipeline# } static Void contributeHttpPipeline(OrderedConfig conf, AuthFilter myAuthFilter) { conf.addOrdered("AuthFilter", myAuthFilter, ["after: BedSheetFilters"]) }
Request Routing
If a request makes it through the filters, it is handed to the Routes service for processing. Confiugre the service by contributing Route instances. Example:
@Contribute { serviceType=Routes# } static Void contributeRoutes(OrderedConfig conf) { conf.add(Route(`/home`, Redirect.movedTemporarily(`/index`))) conf.add(Route(`/index`, IndexPage#service)) }
Routes 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 uri segments are matched to the method arguments. See Route for more details.
Using Draft Routes
If you prefer the draft style of routing, that's no problem, you can use draft Routes in BedSheet
!
Add afBedSheetDraft and draft as dependencies in your build.fan
; now you can contribute draft Route objects in your AppModule
:
using afIoc using afBedSheet using afBedSheetDraft using draft::Route as DraftRoute class AppModule { @Contribute { serviceType=DraftRoutes# } static Void contributeDraftRoutes(OrderedConfig conf) { conf.add(DraftRoute("/", "GET", PageHandler#index)) conf.add(DraftRoute("/echo/{name}/{age}", "GET", PageHandler#print)) } }
Routing lesson over.
(...you Aussies may stop giggling now.)
Request Handling
Request handlers process logic. Generally, they shouldn't pipe anything to the HTTP response OutStream, but instead return a response object for further processing. For example, the quick start HelloPage
handler returns a Text object.
Request handlers are usually written by yourself, 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 Processing
ResponseProcessors process return values from request handlers (sys::File, Text, etc...) and send data to the client. ResponseProcessors
should return true
if no further processing should performed on the http request. Or they can return another response object for further processing, such as a Text obj.
By default, BedSheet handles the following response objects:
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 Error Processing.)
- InStream : The
InStream
is piped to the client. TheInStream
is 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 the given sys::MimeType.
Template Rendering
Templating, or generating content (of HTML or otherwise) is left for other 3rd party libraries and is not a conern of BedSheet
. That said, integrating them into BedSheet
is relatively simple.
Alien-Factory provides the following libraries:
- afSlim for rendering HTML,
- afPillow for integrating afEfanXtra components (may be used with
Slim
!), - afBedSheetEfan for integrating efan (Embedded Fantom) templates and
- afBedSheetMoustache for integrating Mustache templates.
Taking Slim as an example, simply inject and use it in your request handlers:
using afIoc using afBedSheet using afSlim class IndexPage { @Inject Slim? slim Text render() { html := slim.renderFromFile(`xmas.html.slim`.toFile) return Text.fromHtml(html) } }
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 for further processing (for example, Text). Or it may return true
if the error has been completely handled and no further processing is required.
BedSheet
bundles with the follow ErrProcessors
:
- HttpStatusErr : Returns the wrapped HttpStatus for further processing.
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 most non-developers. 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
ENV
with the valuedevelopment
to ensure you continue to see the BedSheet's verbose Err50 page. See this Fantom-Factory article for more details.
To add your own Err 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 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:
@Contribute { serviceType=HttpStatusProcessors# } static Void contributeHttpStatusProcessors(MappedConfig conf) { conf[404] = conf.autobuild(My404Handler#) }
Config Injection
BedSheet uses IocConfig 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 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(MappedConfig conf) { conf[BedSheetConfigIds.httpRequestLogDir] = `/my/log/dir/` conf[BedSheetConfigIds.httpRequestLogFilenamePattern] = "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.httpRequestLogFields
, 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 BedSheetConfigIds.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 BedSheetConfigIds.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 BedSheetConfigIds.responseBufferThreshold for more details.
Testing Your Web App
BedSheet ships with a means of testing your web app without starting the wisp server. This forgoes the overhead of starting a real web server, opening real ports and making real http requests. Essentially you're testing the entire app, just cutting out the middle man.
Use BedServer to start an instance of BedSheet, and then the re-usable BedClient to serve (fake) http requests. The benifits (besides the obvious performance ones) include:
- override real services with test servies / configuration
- inject your Ioc services direct into your test class
- inspect your client's web::WebSession
- cookies are auto sent on each http request
See below for a test example:
using afIoc using afBedSheet class TestWebApp : Test { BedServer? server @Inject UserDAO? userDAO // inject your services override Void setup() { server = BedServer(AppModule#) server.addModule(TestOverrides#) // override services and config with test values server.startup server.injectIntoFields(this) // inject your services } override Void teardown() { server.shutdown } Void testIndexPage() { // given client := server.makeClient // when res := client.get(`/index`) // then verify(res.asStr.contains("Welcome!")) } }
Tips
All request 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 request 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.2.4
- Chg: Upgraded to afIoc-1.5.0.
- Chg: Upgraded to afIocConfig-1.0.0.
v1.2.2
- New: Added
gzip
compression for web fonts. - New: BedSheet connection details printed on startup.
- Chg:
FileHandler
now lets non-existant files fall through. - Chg:
FileHandler
auto addsRoute
mappings to theRoutes
service. - Chg: Added more info to the BedSheet 404 page in dev.
- Chg: Gave more control over the verbose rendering of the standard
BedSheet
pages. - Bug:
BedServer
generated 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 IocEnv
- Chg:
HttpRequestLogFilter
is now in the Http Pipeline by default - it just needs enabling. - Chg: The detailed BedSheet Err500 page is disabled in
production
environments. - Chg: Rejigged how the default
ErrProcessor
is used, making it easier to plug in your own. (Breaking change.) - Chg:
BedSheetConfigIds
renamed fromConfigIds
. (Breaking change.) - Chg: Removed Route Matching -
Routes
now only takeRoute
objects. (Breaking change.) - Chg: Removed
IeAjaxCacheBustingFilter
with no replacement. (Breaking change.) - Chg: Removed
CorsHandler
with 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
AppModule
type not found on startup. - Chg: Disabled afIoc service list on startup.
- Bug:
BedServer
would crash if the app requiredBedSheetMetaData
.
v1.1.2
- New: Added
Causes
section 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
-noTransDeps
startup option now propogates through the proxy.
v1.1.0
- New: Added
BedSheetMetaData
with information on whichAppModule
afbedSheet was started with. - Chg: Renamed
RouteHandler
->MethodInvoker
. (Breaking change.) - Chg: Injectable services are now documented with
(Service)
. - Chg: Moved internal proxy options in
Main
to their own class. - Chg: Enabled multi-line quotes.
- Bug:
afIocConfig
was not always added as a transitive dependency. (Thanks toLightDye
for reporting.)
v1.0.16
- New: Added
Available Values
section to Err500 page, fromafIoc::NotFoundErr
. - Chg: Broke @Config code out into its own module: afIocConfig.
- Chg: Added a skull logo to the
Err500
page. - Chg: Rejigged the
Err500
section layout and tweaked the source code styling.
v1.0.14
- New:
SrcCodeErrs
from 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 Trace
section to Err500 page. - New: Added
Moustache Compilation Err
section 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
null
values. 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.statusCode is now a field.
- Chg: HttpResponse.disableGzip is now a field.
- Chg: HttpResponse.disableBuffering is now a field.
v1.0.4
v1.0.2
- New: Initial release