This document follows on from Part One. You should read that document first to get an understanding of this one. Part Three makes no changes to the server code, but discusses some advantages of continuation based frameworks. Part Four extends the framework, adding a callback system, and fixing an important bug. The modal-web-server-0.2.tar.gz file contains the updated version used in this document.
An important part of any web application is getting input from the user. HTML forms are the common way of doing this. I mentioned briefly in part 1 that the results of a POST request are returned by 'show'. To demonstrate this we'll use the following function to request the user to enter a name:
(define (get-name)
(lambda (url)
(sxml->html-string
`(html
(head (title "Enter your name"))
(body
(form (@ (action ,url) (method "post"))
(input (@ (type "text") (name "name") (size "20")))
(input (@ (type "submit")))))))))
This function presents a standard HTML form. The input field to hold the name has the name 'name'. The following registers a function that requests input of the name and then displays that name with the 'show-message' function we've used previously:
(register-function
(lambda ()
(let ((result (show (get-name))))
(show-message (string-append "Hi " (cdr (assoc "name" result)))))))
Here we assign the result of 'show' to the 'result' variable. For the result of a POST request this will be a list of pairs, each pair being the name of an HTML input and the value of that input. 'assoc' is the function used to pull the pair out of the association list.
The conversion of the form data to the list is done by the Chicken Scheme HTTP extension when it encounters 'application/x-www-form-urlencoded' encoded data. The HTTP extension installs a 'content-parser' for this. We could just as easily install another content parser for this type, replacing the original, to do something else (like return a hashtable for instance).
Another change was made to the framework in 0.2 to handle a problem that can occur if the user hits 'refresh' on the browser after posting the results of a form. If you try the example above in v0.1, after posting the form and on the 'Hi' page, if you hit 'Refresh' the browser will warn you with something like:
The page you are trying to view is the result of posted form data. If you resend the data, any action the form carried out (such as search or online purchase) will be repeated.
This is because the last request was a 'POST'. A 'refresh' request will result in the continuation being called again, which resumes from the end of that post. Any action taken between that post returning and the current 'show' will be called again. This can result in duplicate database transactions, or similar problems, occurring:
(register-function
(lambda ()
(let ((result (show (get-name))))
(anything-run-here-will-be-run-again-on-a-refresh)
(show-message (string-append "Hi " (cdr (assoc "name" result)))))))
When sitting on the 'Hi' page, a 'refresh' will cause 'anything-run-here...' to be run again. Subsequent refreshes will again cause it to be run a third time, etc.
There is a standard 'pattern' for working around this problem and in the 0.2 version of the framework I've implemented this. If you look at 'show' in this current version you will see that it has a call to 'redirect-to-here' at the beginning:
(define (show page)
(redirect-to-here)
(call/cc
(lambda (k)
(let ((kid (get-unique-continuation-id)))
(hash-table-set! kid-registry kid k)
(http:add-resource
(string-append "/" (symbol->string kid))
(lambda (r a)
(call/cc
(lambda (exit)
(suicide exit)
(let ((k (hash-table-ref kid-registry kid)))
(k (http:request-body r)))))))
(process-page-function-result (page (symbol->string kid)))
((suicide) #f)))))
'redirect-to-here' is a function that captures the current continuation and sends a redirect request to the browser to go to it:
;; Force a redirect to the point. This is usually used in 'show' and
;; happens on every request. What this does is force a redirect just
;; before the show. When the user refreshes a page they always get the
;; continuation following this redirect and prevents the browser
;; message 'You are about to repost...' as a result of refreshing the
;; result of a POST request. It also prevents running twice any of the
;; code between the last show and the next show due to a refresh.
(define (redirect-to-here)
(call/cc
(lambda (k)
(let ((kid (get-unique-continuation-id)))
(hash-table-set! kid-registry kid k)
(http:add-resource
(string-append "/" (symbol->string kid))
(lambda (r a)
(call/cc
(lambda (exit)
(suicide exit)
(let ((k (hash-table-ref kid-registry kid)))
(k (http:request-body r)))))))
(process-page-function-result
(sxml->html-string
`(html
(head (meta (@ (http-equiv "refresh")
(content ,(format "0;URL=~a" (symbol->string kid)))))))))
((suicide) #f)))))
The code is very similar to 'show' and cries out for refactoring. I've done it in full to make it clear what is happening. I'm also using a 'META' refresh rather than an HTTP forward since I'm not sure how to do the latter in the Chicken HTTP framework yet. I'll update it in a later version.
By doing this refresh it forces the browser to do a 'GET', immediately after the post, and effectively sets the continuation that will be 'refreshed' to just before the 'show' that is about to happen. So a 'refresh' from the user will cause no doubling up of any transactions that occur between 'show' calls.
Try it out in version 0.2 of the framework. Comment out the 'redirect-to-here' call in 'show' and try it. Then put it back in, re-register the function, and see what the difference with 'refresh' is.
I first learnt about this technique from Seaside and have since seen it described in a number of frameworks as a way to work around the post-refresh-get problem.
Copyright (c) 2004, Chris Double. All Rights Reserved.