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:

  1. Takes an HTTP procedure (like http-request)
  2. Returns a new function that opens a non-blocking socket
  3. 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.