Building an Async HTTPS Client with Goblins and Fibers › Fibers-Only HTTP Client [#253]
Building an Async HTTPS Client with Goblins and Fibers › Fibers-Only HTTP Client [#253]
Now we can create a wrapper that opens a non-blocking socket and passes it to the HTTP procedure:
(define (wrap-http-nonblocking http-proc)
(λ (uri . args)
(let ([port (open-socket-for-uri
uri
#:configure-socket use-nonblocking-i/o!)])
(apply http-proc uri #:port port args))))
This higher-order function:
- Takes an HTTP procedure (like
http-request) - Returns a new function that opens a non-blocking socket
- Passes that socket to the original procedure
But spawn-fiber doesn't return the result—it returns the fiber object. We need channels to communicate results back. Here's a complete working example that demonstrates true concurrency:
(use-modules (fibers))
(use-modules (fibers channels))
(use-modules (web response))
(use-modules ((web client) #:hide (open-socket-for-uri)))
;;; Borrow open-socket-for-uri from guile-websocket's internal module
(define open-socket-for-uri
(@@ (web socket client) open-socket-for-uri))
;;; Set O_NONBLOCK flag on a port
(define (use-nonblocking-i/o! port)
(fcntl port F_SETFL
(logior O_NONBLOCK (fcntl port F_GETFL))))
;;; Wrap an HTTP procedure to use non-blocking sockets
(define (wrap-http-nonblocking http-proc)
(λ (uri . args)
(let ([port (open-socket-for-uri
uri
#:configure-socket use-nonblocking-i/o!)])
(apply http-proc uri #:port port args))))
;;; Make a delayed request and send result to channel
(define (make-delayed-request delay-seconds channel)
(spawn-fiber
(λ ()
(display (format #f "[~a] Starting request with ~a second delay~%"
(strftime "%H:%M:%S" (localtime (current-time)))
delay-seconds))
(define-values (response body)
((wrap-http-nonblocking http-request)
(string-append "https://httpbin.org/delay/"
(number->string delay-seconds))))
(display (format #f "[~a] Completed ~a second delay request, status: ~a~%"
(strftime "%H:%M:%S" (localtime (current-time)))
delay-seconds
(response-code response)))
(put-message channel (cons delay-seconds response)))))
;;; Run concurrent requests with different delays
(define result-channel (make-channel))
(run-fibers
(λ ()
(display "Starting 3 concurrent requests with delays of 5s, 2s, and 3s...")
(newline)
(display (format #f "[~a] Main fiber starting~%~%"
(strftime "%H:%M:%S" (localtime (current-time)))))
;; Spawn three fibers
(make-delayed-request 5 result-channel)
(make-delayed-request 2 result-channel)
(make-delayed-request 3 result-channel)
;; Collect results - they'll arrive in completion order (2s, 3s, 5s)
(let loop ([n 0])
(when (< n 3)
(define result (get-message result-channel))
(display (format #f "[~a] Received result from ~as delay request~%"
(strftime "%H:%M:%S" (localtime (current-time)))
(car result)))
(loop (1+ n))))
(display "\nAll requests completed!"))
#:drain? #t)
When you run this, you'll see the 2-second request complete first, then 3-second, then 5-second—proving the fibers are truly concurrent. The total execution time is ~5 seconds, not 10 seconds.