emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/llm 1f3b018dcb 03/71: Merge pull request #26 from r0man


From: ELPA Syncer
Subject: [elpa] externals/llm 1f3b018dcb 03/71: Merge pull request #26 from r0man/plz
Date: Fri, 17 May 2024 00:58:42 -0400 (EDT)

branch: externals/llm
commit 1f3b018dcb9870667ed92c5d9c14190701cb3dfb
Merge: 9c33eb4a91 a9cd296cd8
Author: Andrew Hyatt <ahyatt@gmail.com>
Commit: GitHub <noreply@github.com>

    Merge pull request #26 from r0man/plz
    
    Add Plz
---
 llm-request-plz.el  | 146 +++++++++
 plz-event-source.el | 458 ++++++++++++++++++++++++++
 plz-media-type.el   | 341 ++++++++++++++++++++
 plz.el              | 912 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1857 insertions(+)

diff --git a/llm-request-plz.el b/llm-request-plz.el
new file mode 100644
index 0000000000..d103f10448
--- /dev/null
+++ b/llm-request-plz.el
@@ -0,0 +1,146 @@
+;;; llm-request-plz.el --- Curl request handling code -*- lexical-binding: t; 
package-lint-main-file: "llm.el"; -*-
+
+;; Copyright (c) 2023  Free Software Foundation, Inc.
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation; either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;; This file provides basic functions for providers who need to request data. 
It
+;; assumes the server is using json.
+
+;;; Code:
+(require 'cl-macs)
+(require 'json)
+(require 'plz-event-source)
+(require 'plz-media-type)
+(require 'rx)
+(require 'url-http)
+
+(defcustom llm-request-plz-timeout 60
+  "The number of seconds to wait for a response from a HTTP server.
+
+Request timings are depending on the request. Requests that need
+more output may take more time, and there is other processing
+besides just token generation that can take a while. Sometimes
+the LLM can get stuck, and you don't want it to take too long.
+This should be balanced to be good enough for hard requests but
+not very long so that we can end stuck requests."
+  :type 'integer
+  :group 'llm)
+
+(cl-defun llm-request-plz-sync-raw-output (url &key headers data timeout)
+  "Make a request to URL.  The raw text response will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+TIMEOUT is the number of seconds to wait for a response."
+  (condition-case error
+      (plz-media-type-request
+        'post url
+        :as `(media-types ,plz-media-types)
+        :body (when data
+                (encode-coding-string (json-encode data) 'utf-8))
+        :headers (append headers '(("Content-Type" . "application/json")))
+        :timeout (or timeout llm-request-plz-timeout))
+    (plz-error
+     (seq-let [error-sym message data] error
+       (cond
+        ((eq 'plz-http-error error-sym)
+         (let ((response (plz-error-response data)))
+           (error "LLM request failed with code %d: %s (additional 
information: %s)"
+                  (plz-response-status response)
+                  (nth 2 (assq (plz-response-status response) url-http-codes))
+                  (plz-response-body response))))
+        ((and (eq 'plz-curl-error error-sym)
+              (eq 28 (car (plz-error-curl-error data))))
+         (error "LLM request timed out"))
+        (t (signal error-sym (list message data))))))))
+
+(cl-defun llm-request-plz-sync (url &key headers data timeout)
+  "Make a request to URL.  The parsed response will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+TIMEOUT is the number of seconds to wait for a response."
+  (llm-request-plz-sync-raw-output url
+                                   :headers headers
+                                   :data data
+                                   :timeout timeout))
+
+(cl-defun llm-request-plz-async (url &key headers data on-success 
on-success-raw on-error _on-partial event-stream-handlers timeout)
+  "Make a request to URL.
+Nothing will be returned.
+
+HEADERS will be added in the Authorization header, in addition to
+standard json header. This is optional.
+
+DATA will be jsonified and sent as the request body.
+This is required.
+
+ON-SUCCESS will be called with the response body as a json
+object. This is optional in the case that ON-SUCCESS-DATA is set,
+and required otherwise.
+
+ON-ERROR will be called with the error code and a response-body.
+This is required.
+
+ON-PARTIAL will be called with the potentially incomplete response
+body as a string.  This is an optional argument.
+
+ON-SUCCESS-RAW, if set, will be called in the buffer with the
+response body, and expect the response content. This is an
+optional argument, and mostly useful for streaming.  If not set,
+the buffer is turned into JSON and passed to ON-SUCCESS."
+  (plz-media-type-request
+    'post url
+    :as `(media-types ,(cons (cons "text/event-stream"
+                                   (plz-media-type:text/event-stream
+                                    :events event-stream-handlers))
+                             plz-media-types))
+    :body (when data
+            (encode-coding-string (json-encode data) 'utf-8))
+    :headers (append headers
+                     '(("Accept-encoding" . "identity")
+                       ("Content-Type" . "application/json")))
+    :then (lambda (response)
+            (when on-success-raw
+              (user-error "Not supported yet: on-success-raw"))
+            (when on-success
+              (funcall on-success response)))
+    :else (lambda (error)
+            (when on-error
+              (funcall on-error error)))
+    :timeout (or timeout llm-request-plz-timeout)))
+
+;; This is a useful method for getting out of the request buffer when it's time
+;; to make callbacks.
+(defun llm-request-plz-callback-in-buffer (buf f &rest args)
+  "Run F with ARSG in the context of BUF.
+But if BUF has been killed, use a temporary buffer instead.
+If F is nil, nothing is done."
+  (when f
+    (if (buffer-live-p buf)
+        (with-current-buffer buf (apply f args))
+      (with-temp-buffer (apply f args)))))
+
+(provide 'llm-request-plz)
+;;; llm-request-plz.el ends here
diff --git a/plz-event-source.el b/plz-event-source.el
new file mode 100644
index 0000000000..c72e7bb7fa
--- /dev/null
+++ b/plz-event-source.el
@@ -0,0 +1,458 @@
+;;; plz-event-source.el --- Server Sent Event Source -*- lexical-binding: t; 
-*-
+
+;; Copyright (C) 2019-2023  Free Software Foundation, Inc.
+
+;; Author: r0man <roman@burningswell.com>
+;; Maintainer: r0man <roman@burningswell.com>
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides a parser and an event source implementation
+;; for the Server Sent Event (SSE) protocol.
+
+;; See: 
https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'eieio)
+(require 'pcase)
+(require 'plz)
+(require 'plz-media-type)
+(require 'rx)
+
+;; Event
+
+(defclass plz-event-source-event ()
+  ((data
+    :accessor plz-event-source-event-data
+    :initarg :data
+    :initform nil
+    :documentation "The event data.")
+   (last-event-id
+    :accessor plz-event-source-event-last-event-id
+    :initarg :last-event-id
+    :initform nil
+    :documentation "The last event id."
+    :type (or null string))
+   (origin
+    :accessor plz-event-source-event-origin
+    :initarg :origin
+    :initform nil
+    :documentation "The event origin."
+    :type (or null string))
+   (type
+    :accessor plz-event-source-event-type
+    :initarg :type
+    :initform "message"
+    :documentation "The event type."
+    :type string))
+  "The server sent event class.")
+
+;; Parser
+
+(defclass plz-event-source-parser ()
+  ((buffer
+    :documentation "The name of the buffer to read events from."
+    :initarg :buffer
+    :type string)
+   (events
+    :initarg :events
+    :initform nil
+    :documentation "The queue of events to dispatch."
+    :type (list-of plz-event-source-event))
+   (data-buffer
+    :initarg :data-buffer
+    :initform ""
+    :documentation "Data buffer."
+    :type string)
+   (event-type-buffer
+    :initarg :event-type-buffer
+    :initform ""
+    :documentation "Event type buffer."
+    :type string)
+   (last-event-id
+    :initarg :last-event-id
+    :initform ""
+    :documentation "Last event id."
+    :type string)
+   (last-event-id-buffer
+    :initarg :last-event-id-buffer
+    :initform ""
+    :documentation "Last event id buffer."
+    :type string)
+   (position
+    :initarg :position
+    :initform 0
+    :type integer
+    :documentation "The position in the buffer."
+    :type integer))
+  "The server sent event stream parser.")
+
+(defconst plz-event-source--end-of-line-regexp
+  (rx (or "\r\n" "\n" "\r"))
+  "Regular expression matching the end of a line.")
+
+(defconst plz-event-source--line-regexp
+  (rx (* not-newline) (or "\r\n" "\n" "\r"))
+  "Regular expression matching a line of the event source stream.")
+
+(defun plz-event-source--parse-bom (line)
+  "Parse the Byte Order Mark (BOM) from LINE."
+  (if (string-prefix-p "\uFEFF" line)
+      (substring line 1)
+    line))
+
+(defun plz-event-source--looking-at-line-p ()
+  "Return non-nil if the current line matches the event source line regexp."
+  (looking-at plz-event-source--line-regexp))
+
+(defun plz-event-source--parse-line ()
+  "Return non-nil if the current line matches the event source line regexp."
+  (when (looking-at plz-event-source--line-regexp)
+    (string-trim-right (delete-and-extract-region (match-beginning 0) 
(match-end 0))
+                       plz-event-source--end-of-line-regexp)))
+
+(defun plz-event-source--dispatch-event (parser)
+  "Dispatch an event from PARSER to registered listeners."
+  (with-slots (data-buffer event-type-buffer events last-event-id 
last-event-id-buffer) parser
+    (setf last-event-id last-event-id-buffer)
+    (if (string-empty-p data-buffer)
+        (setf data-buffer ""
+              event-type-buffer "")
+      (progn
+        (setf data-buffer (string-trim-right data-buffer "\n"))
+        (let ((event (plz-event-source-event
+                      :data data-buffer
+                      :last-event-id (unless (string-blank-p last-event-id)
+                                       last-event-id)
+                      :origin (buffer-name)
+                      :type (if (string-blank-p event-type-buffer)
+                                "message"
+                              event-type-buffer))))
+          (setf data-buffer ""
+                event-type-buffer "")
+          (setf events (cons event events))
+          event)))))
+
+(defun plz-event-source--process-event (parser field value)
+  "Process the FIELD and VALUE from PARSER as a event."
+  (ignore field)
+  (with-slots (event-type-buffer) parser
+    (setf event-type-buffer value)))
+
+(defun plz-event-source--process-data (parser field value)
+  "Process the FIELD and VALUE from PARSER as data."
+  (ignore field)
+  (with-slots (data-buffer) parser
+    (setf data-buffer (concat data-buffer value "\n"))))
+
+(defun plz-event-source--process-id (parser field value)
+  "Process the FIELD and VALUE from PARSER as event id."
+  (ignore field)
+  (unless (string-match "\u0000" value)
+    (with-slots (last-event-id-buffer) parser
+      (setf last-event-id-buffer value))))
+
+(defun plz-event-source--process-retry (parser field value)
+  "Process the FIELD and VALUE from PARSER as event id."
+  (ignore parser)
+  (message "TODO: Process retry for field %s and value %s." field value))
+
+(defun plz-event-source--process-field (parser field value)
+  "Process the FIELD and VALUE from PARSER."
+  (cond ((equal "event" field)
+         (plz-event-source--process-event parser field value))
+        ((equal "data" field)
+         (plz-event-source--process-data parser field value))
+        ((equal "id" field)
+         (plz-event-source--process-id parser field value))
+        ((equal "retry" field)
+         (plz-event-source--process-retry parser field value))))
+
+(defun plz-event-source--process-line (parser line)
+  "Parse a LINE of the event stream PARSER and dispatch events."
+  (cond ((string-prefix-p ":" line))
+        ((string-blank-p line)
+         (plz-event-source--dispatch-event parser))
+        ((string-match ":" line)
+         (let ((field (substring line 0 (match-beginning 0)))
+               (value (substring line (match-end 0))))
+           (plz-event-source--process-field parser field
+                                            (if (string-prefix-p " " value)
+                                                (substring value 1)
+                                              value))))
+        (t (plz-event-source--process-field parser line ""))))
+
+(defun plz-event-source-parse-line (parser)
+  "Parse a line from the event stream in the PARSER buffer."
+  (with-slots (buffer position) parser
+    (with-current-buffer buffer
+      (save-excursion
+        (goto-char position)
+        (when-let (line (plz-event-source--parse-line))
+          (setf position (point))
+          (plz-event-source--process-line parser line)
+          line)))))
+
+(defun plz-event-source-parse-stream (parser)
+  "Parse the event stream in the the PARSER buffer."
+  (with-slots (buffer handlers) parser
+    (with-current-buffer (get-buffer buffer)
+      (goto-char (point-min))
+      (while (not (eobp))
+        (when-let (line (plz-event-source--parse-line))
+          (plz-event-source--process-line parser line))))))
+
+(defun plz-event-source-parser-insert (parser string)
+  "Insert STRING into the buffer of the event PARSER."
+  (with-slots (buffer events position) parser
+    (with-current-buffer (get-buffer buffer)
+      (insert string)
+      (while (plz-event-source-parse-line parser))
+      events)))
+
+;; Event Source
+
+(defclass plz-event-source ()
+  ((errors
+    :initarg :errors
+    :documentation "The errors of the event source.")
+   (handlers
+    :initarg :handlers
+    :initform nil
+    :documentation "Registered event handlers.")
+   (last-event-id
+    :initarg :last-event-id
+    :initform ""
+    :documentation "Last event id.")
+   (options
+    :initarg :options
+    :documentation "The url of the event source."
+    :type list)
+   (ready-state
+    :documentation "The ready state of the event source."
+    :initarg :ready-state
+    :initform 'closed
+    :type (member closed connecting open))
+   (url
+    :initarg :url
+    :documentation "The url of the event source."
+    :type (or null string)))
+  "The server sent event source class.")
+
+(cl-defgeneric plz-event-source-open (source)
+  "Open the event SOURCE.")
+
+(cl-defgeneric plz-event-source-close (source)
+  "Close the event SOURCE.")
+
+(cl-defgeneric plz-event-source-insert (source data)
+  "Insert DATA into the event SOURCE buffer, parse and dispatch events.")
+
+(defun plz-event-source-add-listener (source type listener)
+  "Add an event LISTENER for event TYPE to the event SOURCE."
+  (with-slots (handlers) source
+    (setf handlers (append handlers (list (cons type listener))))
+    source))
+
+(defun plz-event-source-remove-listener (source type listener)
+  "Remove an event LISTENER for event TYPE from the event SOURCE."
+  (with-slots (handlers) source
+    (setf handlers (cl-remove-if (lambda (pair)
+                                   (and (eq (car pair) type)
+                                        (eq (cdr pair) listener)))
+                                 handlers))
+    source))
+
+(defun plz-event-source-dispatch-event (source event)
+  "Dispatch the EVENT to the listeners of event SOURCE."
+  (with-slots (handlers) source
+    (dolist (pair handlers)
+      (when (equal (car pair) (oref event type))
+        (funcall (cdr pair) source event)))))
+
+(defun plz-event-source-dispatch-events (source events)
+  "Dispatch the EVENTS to the listeners of event SOURCE."
+  (dolist (event (reverse events))
+    (plz-event-source-dispatch-event source event)))
+
+(defun plz-event-source--response-in-buffer-p ()
+  "Return non-nil the if point is looking at a HTTP response."
+  (save-excursion
+    (goto-char (point-min))
+    (re-search-forward plz-http-end-of-headers-regexp nil t)))
+
+(defun plz-event-source-parser--end-of-headers ()
+  "Return the end of headers position in the current buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (re-search-forward plz-http-end-of-headers-regexp nil t)
+    (point)))
+
+;; Buffer event source
+
+(defclass plz-buffer-event-source (plz-event-source)
+  ((buffer
+    :initarg :buffer
+    :documentation "The event source buffer."
+    :type string)
+   (parser
+    :initarg :parser
+    :documentation "The event source parser."
+    :type (or null plz-event-source-parser)))
+  "A server sent event source using curl for HTTP.")
+
+(cl-defmethod plz-event-source-insert ((source plz-buffer-event-source) data)
+  "Insert DATA into the event SOURCE buffer, parse and dispatch events."
+  (with-slots (parser) source
+    (plz-event-source-parser-insert parser data)
+    (with-slots (events) parser
+      (plz-event-source-dispatch-events source events)
+      (setf events nil))))
+
+(defun plz-event-source--buffer-start-position ()
+  "Return the start position of the current buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (re-search-forward plz-http-end-of-headers-regexp nil t)
+    (point)))
+
+(cl-defmethod plz-event-source-open ((source plz-buffer-event-source))
+  "Open a connection to the URL of the event SOURCE."
+  (with-slots (buffer errors options ready-state parser) source
+    (with-current-buffer (get-buffer-create buffer)
+      (let ((event (plz-event-source-event :type "open")))
+        (setf ready-state 'connecting)
+        (setf parser (plz-event-source-parser
+                      :buffer buffer
+                      :position (plz-event-source--buffer-start-position)))
+        (setf ready-state 'open)
+        (plz-event-source-dispatch-event source event)
+        source))))
+
+(cl-defmethod plz-event-source-close ((source plz-buffer-event-source))
+  "Close the connection of the event SOURCE."
+  (with-slots (buffer ready-state) source
+    (let ((event (plz-event-source-event :type "close")))
+      (setf ready-state 'closed)
+      (plz-event-source-dispatch-event source event)
+      source)))
+
+(defclass plz-http-event-source (plz-event-source)
+  ((process
+    :initarg :process
+    :documentation "The process of the event source."
+    :type (or null process))
+   (response
+    :initarg :response
+    :documentation "The plz HTTP response."
+    :type (or null plz-response)))
+  "A server sent event source using curl for HTTP.")
+
+(defun plz-event-source--media-types (source)
+  "Return the media types of the event SOURCE."
+  (with-slots (handlers) source
+    (let ((media-type (plz-media-type:text/event-stream :events handlers)))
+      (cons (cons "text/event-stream" media-type) plz-media-types))))
+
+(cl-defmethod plz-event-source-open ((source plz-http-event-source))
+  "Open a connection to the URL of the event SOURCE."
+  (with-slots (errors options process ready-state response url) source
+    (setf ready-state 'connecting)
+    (setf response nil)
+    (setf process (plz-media-type-request
+                    (or (alist-get 'method options) 'get) url
+                    :as `(media-types ,(plz-event-source--media-types source))
+                    :body (alist-get 'body options)
+                    :headers (alist-get 'headers options)
+                    :then (lambda (object)
+                            (setf response object))
+                    :else (lambda (object)
+                            (setf errors (push object errors))
+                            (setf response (plz-error-response object)))
+                    :finally (lambda ()
+                               (setf ready-state 'closed))))
+    source))
+
+(cl-defmethod plz-event-source-close ((source plz-http-event-source))
+  "Close the connection of the event SOURCE."
+  (with-slots (process ready-state) source
+    (delete-process process)
+    (setf ready-state 'closed)))
+
+;; Content Type: text/event-stream
+
+(defclass plz-media-type:text/event-stream 
(plz-media-type:application/octet-stream)
+  ((name :initform "text/event-stream")
+   (events :documentation "Association list from event type to handler."
+           :initarg :events)))
+
+(defun plz-media-type:text/event-stream--event-source (response)
+  "Return the event source of the RESPONSE."
+  (process-get (plz-response-process response) :plz-event-source))
+
+(cl-defmethod plz-media-type-else ((_ plz-media-type:text/event-stream) error)
+  "Transform the ERROR into a format suitable for MEDIA-TYPE."
+  (let* ((response (plz-error-response error))
+         (source (plz-media-type:text/event-stream--event-source response))
+         (event (plz-event-source-event :type "error" :data error)))
+    (plz-event-source-close source)
+    (plz-event-source-dispatch-event source event)
+    error))
+
+(cl-defmethod plz-media-type-process ((media-type 
plz-media-type:text/event-stream) process chunk)
+  "Process the CHUNK according to MEDIA-TYPE using PROCESS."
+  (when (buffer-live-p (process-buffer process))
+    (with-current-buffer (process-buffer process)
+      (unless (process-get process :plz-event-source)
+        (let* ((response (make-plz-response
+                          :status (plz-response-status chunk)
+                          :headers (plz-response-headers chunk)))
+               (source (plz-event-source-open
+                        (plz-buffer-event-source
+                         :buffer (buffer-name (process-buffer process))
+                         :handlers (seq-map
+                                    (lambda (pair)
+                                      (let ((type (car pair))
+                                            (handler (cdr pair)))
+                                        (cond
+                                         ((equal "open" type)
+                                          (cons type (lambda (source event)
+                                                       (setf (oref event data) 
response)
+                                                       (funcall handler source 
event))))
+                                         ((equal "close" type)
+                                          (cons type (lambda (source event)
+                                                       (setf (oref event data) 
response)
+                                                       (funcall handler source 
event))))
+                                         (t pair))))
+                                    (oref media-type events))))))
+          (process-put process :plz-event-source source)))
+      (plz-event-source-insert (process-get process :plz-event-source)
+                               (plz-response-body chunk)))))
+
+(cl-defmethod plz-media-type-then ((_ plz-media-type:text/event-stream) 
response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+  (let ((source (plz-media-type:text/event-stream--event-source response)))
+    (plz-event-source-close source)
+    response))
+
+(provide 'plz-event-source)
+;;; plz-event-source.el ends here
diff --git a/plz-media-type.el b/plz-media-type.el
new file mode 100644
index 0000000000..af498ce87f
--- /dev/null
+++ b/plz-media-type.el
@@ -0,0 +1,341 @@
+;;; plz-media-type.el --- plz content types -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019-2023  Free Software Foundation, Inc.
+
+;; Author: r0man <roman@burningswell.com>
+;; Maintainer: r0man <roman@burningswell.com>
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file handles content type.
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'eieio)
+(require 'plz)
+
+(defclass plz:media-type ()
+  ((name
+    :documentation "The MIME Type of the handler."
+    :initarg :name
+    :initform "application/octet-stream"
+    :type string)))
+
+(cl-defgeneric plz-media-type-else (media-type error)
+  "Transform the ERROR into a format suitable for MEDIA-TYPE.")
+
+(cl-defgeneric plz-media-type-then (media-type response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE.")
+
+(cl-defgeneric plz-media-type-process (media-type process chunk)
+  "Process the CHUNK according to MEDIA-TYPE using PROCESS.")
+
+(defun plz-media-type--content-type (response)
+  "Return the content type header of RESPONSE, or nil if it's not set."
+  (let ((headers (plz-response-headers response)))
+    (when-let (header (cdr (assoc 'content-type headers)))
+      (replace-regexp-in-string "\s*\\(;.*\\)?" "" header))))
+
+(defun plz-media--type-find (media-types media-type)
+  "Lookup the MEDIA-TYPE in MEDIA-TYPES."
+  (or (alist-get media-type media-types nil nil #'equal)
+      (alist-get t media-types)
+      (plz-media-type:application/octet-stream)))
+
+(defun plz-media-type--of-response (media-types response)
+  "Lookup the content type of RESPONSE in MEDIA-TYPES."
+  (let ((media-type (plz-media-type--content-type response)))
+    (plz-media--type-find media-types media-type)))
+
+(defun plz-media-type-process-filter (process media-types chunk)
+  "The process filter that handles different content types.
+
+PROCESS is the process.
+
+MEDIA-TYPES is an association list from media type to an
+instance of a content type class.
+
+CHUNK is a part of the HTTP body."
+  (when (buffer-live-p (process-buffer process))
+    (with-current-buffer (process-buffer process)
+      (let ((moving (= (point) (process-mark process))))
+        (if-let (media-type (process-get process :plz-media-type))
+            (let ((response (process-get process :plz-media-type-response)))
+              (setf (plz-response-body response) chunk)
+              (plz-media-type-process media-type process response))
+          (progn
+            (save-excursion
+              (goto-char (process-mark process))
+              (insert chunk)
+              (set-marker (process-mark process) (point)))
+            (goto-char (point-min))
+            (when (re-search-forward plz-http-end-of-headers-regexp nil t)
+              (let ((body-start (point)))
+                (goto-char (point-min))
+                (let* ((response (prog1 (plz--response) (widen)))
+                       (media-type (plz-media-type--of-response media-types 
response)))
+                  (process-put process :plz-media-type media-type)
+                  (when-let (body (plz-response-body response))
+                    (when (> (length body) 0)
+                      (delete-region body-start (point-max))
+                      (set-marker (process-mark process) (point))
+                      (plz-media-type-process media-type process response)))
+                  (setf (plz-response-body response) nil)
+                  (process-put process :plz-media-type-response response))))))
+        (when moving
+          (goto-char (process-mark process)))))))
+
+;; Content Type: application/octet-stream
+
+(defclass plz-media-type:application/octet-stream (plz:media-type)
+  ((name :initform "application/octet-stream")))
+
+(cl-defmethod plz-media-type-else ((media-type 
plz-media-type:application/octet-stream) error)
+  "Transform the ERROR into a format suitable for MEDIA-TYPE."
+  (let ((response (plz-error-response error)))
+    (setf (plz-error-response error) (plz-media-type-then media-type response))
+    error))
+
+(cl-defmethod plz-media-type-then ((media-type 
plz-media-type:application/octet-stream) response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+  (ignore media-type)
+  response)
+
+(cl-defmethod plz-media-type-process ((media-type 
plz-media-type:application/octet-stream) process chunk)
+  "Process the CHUNK according to MEDIA-TYPE using PROCESS."
+  (ignore media-type)
+  (when (buffer-live-p (process-buffer process))
+    (with-current-buffer (process-buffer process)
+      (let ((moving (= (point) (process-mark process))))
+        (save-excursion
+          (goto-char (process-mark process))
+          (insert (plz-response-body chunk))
+          (set-marker (process-mark process) (point)))
+        (when moving
+          (goto-char (process-mark process)))))))
+
+;; Content Type: application/json
+
+(defclass plz-media-type:application/json 
(plz-media-type:application/octet-stream)
+  ((name :initform "application/json")
+   (array-type :initform 'array)
+   (false-object :initform :json-false)
+   (null-object :initform nil)
+   (object-type :initform 'alist)))
+
+(cl-defmethod plz-media-type-then ((media-type 
plz-media-type:application/json) response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+  (with-slots (array-type false-object null-object object-type) media-type
+    (setf (plz-response-body response)
+          (with-temp-buffer
+            (insert (plz-response-body response))
+            (goto-char (point-min))
+            (json-parse-buffer :array-type array-type
+                               :false-object false-object
+                               :null-object null-object
+                               :object-type object-type)))
+    response))
+
+;; Content Type: application/xml
+
+(defclass plz-media-type:application/xml 
(plz-media-type:application/octet-stream)
+  ((name :initform "application/xml")))
+
+(cl-defmethod plz-media-type-then ((media-type plz-media-type:application/xml) 
response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+  (with-slots (array-type false-object null-object object-type) media-type
+    (setf (plz-response-body response)
+          (with-temp-buffer
+            (insert (plz-response-body response))
+            (libxml-parse-html-region)))
+    response))
+
+;; Content Type: text/html
+
+(defclass plz-media-type:text/html (plz-media-type:application/octet-stream)
+  ((name :initform "text/html")))
+
+(cl-defmethod plz-media-type-then ((media-type plz-media-type:text/html) 
response)
+  "Transform the RESPONSE into a format suitable for MEDIA-TYPE."
+  (with-slots (array-type false-object null-object object-type) media-type
+    (setf (plz-response-body response)
+          (with-temp-buffer
+            (insert (plz-response-body response))
+            (libxml-parse-html-region)))
+    response))
+
+(defvar plz-media-types
+  `(("application/json" . ,(plz-media-type:application/json))
+    ("application/octet-stream" . ,(plz-media-type:application/json))
+    ("application/xml" . ,(plz-media-type:application/xml))
+    ("text/html" . ,(plz-media-type:text/html))
+    (t . ,(plz-media-type:application/octet-stream)))
+  "Alist from media type to content type.")
+
+(cl-defun plz-media-type-request
+    (method
+     url
+     &rest rest &key headers body else finally noquery
+     (as 'string)
+     (body-type 'text)
+     (connect-timeout plz-connect-timeout)
+     (decode t decode-s)
+     (then 'sync)
+     (timeout plz-timeout))
+  "Request METHOD from URL with curl.
+Return the curl process object or, for a synchronous request, the
+selected result.
+
+HEADERS may be an alist of extra headers to send with the
+request.
+
+BODY may be a string, a buffer, or a list like `(file FILENAME)'
+to upload a file from disk.
+
+BODY-TYPE may be `text' to send BODY as text, or `binary' to send
+it as binary.
+
+AS selects the kind of result to pass to the callback function
+THEN, or the kind of result to return for synchronous requests.
+It may be:
+
+- `buffer' to pass the response buffer, which will be narrowed to
+  the response body and decoded according to DECODE.
+
+- `binary' to pass the response body as an un-decoded string.
+
+- `string' to pass the response body as a decoded string.
+
+- `response' to pass a `plz-response' structure.
+
+- `file' to pass a temporary filename to which the response body
+  has been saved without decoding.
+
+- `(file FILENAME)' to pass FILENAME after having saved the
+  response body to it without decoding.  FILENAME must be a
+  non-existent file; if it exists, it will not be overwritten,
+  and an error will be signaled.
+
+- `(stream :through PROCESS-FILTER)' to asynchronously stream the
+  HTTP response.  PROCESS-FILTER is an Emacs process filter
+  function, and must accept two arguments: the curl process
+  sending the request and a chunk of the HTTP body, which was
+  just received.
+
+- A function, which is called in the response buffer with it
+  narrowed to the response body (suitable for, e.g. `json-read').
+
+If DECODE is non-nil, the response body is decoded automatically.
+For binary content, it should be nil.  When AS is `binary',
+DECODE is automatically set to nil.
+
+THEN is a callback function, whose sole argument is selected
+above with AS; if the request fails and no ELSE function is
+given (see below), the argument will be a `plz-error' structure
+describing the error.  Or THEN may be `sync' to make a
+synchronous request, in which case the result is returned
+directly from this function.
+
+ELSE is an optional callback function called when the request
+fails (i.e. if curl fails, or if the HTTP response has a non-2xx
+status code).  It is called with one argument, a `plz-error'
+structure.  If ELSE is nil, a `plz-curl-error' or
+`plz-http-error' is signaled when the request fails, with a
+`plz-error' structure as the error data.  For synchronous
+requests, this argument is ignored.
+
+NOTE: In v0.8 of `plz', only one error will be signaled:
+`plz-error'.  The existing errors, `plz-curl-error' and
+`plz-http-error', inherit from `plz-error' to allow applications
+to update their code while using v0.7 (i.e. any `condition-case'
+forms should now handle only `plz-error', not the other two).
+
+FINALLY is an optional function called without argument after
+THEN or ELSE, as appropriate.  For synchronous requests, this
+argument is ignored.
+
+CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
+how long it takes to connect to a host and to receive a response
+from a host, respectively.
+
+NOQUERY is passed to `make-process', which see.
+
+When the HTTP response is streamed, the buffering in the curl
+output stream is turned off and the PROCESS-FILTER may be called
+multiple times, depending on the size of the HTTP body.  It is
+the user's responsibility to understand and process each chunk,
+and to construct the finalized response if necessary.  There are
+no guarantees regarding the chunk, such as being line-based or
+not.
+\(To silence checkdoc, we mention the internal argument REST.)"
+  ;; FIXME(v0.8): Remove the note about error changes from the docstring.
+  ;; FIXME(v0.8): Update error signals in docstring.
+  (declare (indent defun))
+  (ignore as) ;; TODO: Handle as?
+  (if-let (media-types (pcase as
+                         (`(media-types ,media-types)
+                          media-types)))
+      (let* ((plz-curl-default-args (cons "--no-buffer" plz-curl-default-args))
+             (result (plz method url
+                       :as 'response
+                       :body body
+                       :body-type body-type
+                       :connect-timeout connect-timeout
+                       :decode decode
+                       :else (when (functionp else)
+                               (lambda (object)
+                                 (let* ((media-type 
(plz-media-type--of-response media-types (plz-error-response object)))
+                                        (object (plz-media-type-else 
media-type object)))
+                                   (funcall else object))))
+                       :finally (when (functionp finally)
+                                  (lambda () (funcall finally)))
+                       :headers headers
+                       :noquery noquery
+                       :process-filter (lambda (process chunk)
+                                         (plz-media-type-process-filter 
process media-types chunk))
+                       :timeout timeout
+                       :then (cond
+                              ((symbolp then) then)
+                              ((functionp then)
+                               (lambda (object)
+                                 (let* ((media-type 
(plz-media-type--of-response media-types object))
+                                        (object (plz-media-type-then 
media-type object)))
+                                   (funcall then object))))))))
+        ;; TODO: Handle sync event stream
+        (cond
+         ((processp result)
+          result)
+         ((plz-response-p result)
+          (let ((media-type (plz-media-type--of-response media-types result)))
+            (plz-media-type-then media-type result)))
+         ((plz-error-p result)
+          (let ((media-type (plz-media-type--of-response media-types 
(plz-error-response result))))
+            (plz-media-type-else media-type result)))
+         (t result)))
+    (apply #'plz (append (list method url) rest))))
+
+;;;; Footer
+
+(provide 'plz-media-type)
+
+;;; plz-media-type.el ends here
diff --git a/plz.el b/plz.el
new file mode 100644
index 0000000000..0739a20b04
--- /dev/null
+++ b/plz.el
@@ -0,0 +1,912 @@
+;;; plz.el --- HTTP library                         -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019-2023  Free Software Foundation, Inc.
+
+;; Author: Adam Porter <adam@alphapapa.net>
+;; Maintainer: Adam Porter <adam@alphapapa.net>
+;; URL: https://github.com/alphapapa/plz.el
+;; Version: 0.8-pre
+;; Package-Requires: ((emacs "26.3"))
+;; Keywords: comm, network, http
+
+;; This file is part of GNU Emacs.
+
+;;; License:
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; An HTTP library that uses curl as a backend.  Inspired by, and some
+;; code copied from, Christopher Wellons's library, elfeed-curl.el.
+;;
+;; Why this package?
+;;
+;; 1.  `url' works well for many things, but it has some issues.
+;; 2.  `request' works well for many things, but it has some issues.
+;; 3.  Chris Wellons doesn't have time to factor his excellent
+;;     elfeed-curl.el library out of Elfeed.  This will have to do.
+;;
+;; Why is it called `plz'?
+;;
+;; 1.  There's already a package called `http'.
+;; 2.  There's already a package called `request'.
+;; 3.  Naming things is hard.
+
+;;;; Usage:
+
+;; FIXME(v0.8): Remove the following note.
+
+;; NOTE: In v0.8 of plz, only one error will be signaled: `plz-error'.
+;; The existing errors, `plz-curl-error' and `plz-http-error', inherit
+;; from `plz-error' to allow applications to update their code while
+;; using v0.7 (i.e. any `condition-case' forms should now handle only
+;; `plz-error', not the other two).
+
+;; Call function `plz' to make an HTTP request.  Its docstring
+;; explains its arguments.  `plz' also supports other HTTP methods,
+;; uploading and downloading binary files, sending URL parameters and
+;; HTTP headers, configurable timeouts, error-handling "else" and
+;; always-called "finally" functions, and more.
+
+;; Basic usage is simple.  For example, to make a synchronous request
+;; and return the HTTP response body as a string:
+;;
+;;   (plz 'get "https://httpbin.org/get";)
+;;
+;; Which returns the JSON object as a string:
+;;
+;;   "{
+;;     \"args\": {},
+;;     \"headers\": {
+;;       \"Accept\": \"*/*\",
+;;       \"Accept-Encoding\": \"deflate, gzip\",
+;;       \"Host\": \"httpbin.org\",
+;;       \"User-Agent\": \"curl/7.35.0\"
+;;     },
+;;     \"origin\": \"xxx.xxx.xxx.xxx\",
+;;     \"url\": \"https://httpbin.org/get\";
+;;   }"
+;;
+;; To make the same request asynchronously, decoding the JSON and
+;; printing a message with a value from it:
+;;
+;;   (plz 'get "https://httpbin.org/get"; :as #'json-read
+;;     :then (lambda (alist) (message "URL: %s" (alist-get 'url alist))))
+;;
+;; Which, after the request returns, prints:
+;;
+;;   URL: https://httpbin.org/get
+
+;;;; Credits:
+
+;; Thanks to Chris Wellons for inspiration, encouragement, and advice.
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'rx)
+(require 'subr-x)
+
+;;;; Errors
+
+(define-error 'plz-error "plz error")
+(define-error 'plz-curl-error "plz: Curl error" 'plz-error)
+(define-error 'plz-http-error "plz: HTTP error" 'plz-error)
+
+;;;; Structs
+
+(cl-defstruct plz-response
+  version status headers body process)
+
+(cl-defstruct plz-error
+  curl-error response message)
+
+;;;; Constants
+
+(defconst plz-http-response-status-line-regexp
+  (rx "HTTP/" (group (or "1.0" "1.1" "2")) " "
+      ;; Status code
+      (group (1+ digit)) " "
+      ;; Reason phrase
+      (optional (group (1+ (not (any "\r\n")))))
+      (or
+       ;; HTTP 1
+       "\r\n"
+       ;; HTTP 2
+       "\n"))
+  "Regular expression matching HTTP response status line.")
+
+(defconst plz-http-end-of-headers-regexp
+  (rx (or "\r\n\r\n" "\n\n"))
+  "Regular expression matching the end of HTTP headers.
+This must work with both HTTP/1 (using CRLF) and HTTP/2 (using
+only LF).")
+
+(defconst plz-curl-errors
+  ;; Copied from elfeed-curl.el.
+  '((1 . "Unsupported protocol.")
+    (2 . "Failed to initialize.")
+    (3 . "URL malformed. The syntax was not correct.")
+    (4 . "A feature or option that was needed to perform the desired request 
was not enabled or was explicitly disabled at build-time.")
+    (5 . "Couldn't resolve proxy. The given proxy host could not be resolved.")
+    (6 . "Couldn't resolve host. The given remote host was not resolved.")
+    (7 . "Failed to connect to host.")
+    (8 . "FTP weird server reply. The server sent data curl couldn't parse.")
+    (9 . "FTP access denied.")
+    (11 . "FTP weird PASS reply.")
+    (13 . "FTP weird PASV reply.")
+    (14 . "FTP weird 227 format.")
+    (15 . "FTP can't get host.")
+    (17 . "FTP couldn't set binary.")
+    (18 . "Partial file. Only a part of the file was transferred.")
+    (19 . "FTP couldn't download/access the given file, the RETR (or similar) 
command failed.")
+    (21 . "FTP quote error. A quote command returned error from the server.")
+    (22 . "HTTP page not retrieved.")
+    (23 . "Write error.")
+    (25 . "FTP couldn't STOR file.")
+    (26 . "Read error. Various reading problems.")
+    (27 . "Out of memory. A memory allocation request failed.")
+    (28 . "Operation timeout.")
+    (30 . "FTP PORT failed.")
+    (31 . "FTP couldn't use REST.")
+    (33 . "HTTP range error. The range \"command\" didn't work.")
+    (34 . "HTTP post error. Internal post-request generation error.")
+    (35 . "SSL connect error. The SSL handshaking failed.")
+    (36 . "FTP bad download resume.")
+    (37 . "FILE couldn't read file.")
+    (38 . "LDAP bind operation failed.")
+    (39 . "LDAP search failed.")
+    (41 . "Function not found. A required LDAP function was not found.")
+    (42 . "Aborted by callback.")
+    (43 . "Internal error. A function was called with a bad parameter.")
+    (45 . "Interface error. A specified outgoing interface could not be used.")
+    (47 . "Too many redirects.")
+    (48 . "Unknown option specified to libcurl.")
+    (49 . "Malformed telnet option.")
+    (51 . "The peer's SSL certificate or SSH MD5 fingerprint was not OK.")
+    (52 . "The server didn't reply anything, which here is considered an 
error.")
+    (53 . "SSL crypto engine not found.")
+    (54 . "Cannot set SSL crypto engine as default.")
+    (55 . "Failed sending network data.")
+    (56 . "Failure in receiving network data.")
+    (58 . "Problem with the local certificate.")
+    (59 . "Couldn't use specified SSL cipher.")
+    (60 . "Peer certificate cannot be authenticated with known CA 
certificates.")
+    (61 . "Unrecognized transfer encoding.")
+    (62 . "Invalid LDAP URL.")
+    (63 . "Maximum file size exceeded.")
+    (64 . "Requested FTP SSL level failed.")
+    (65 . "Sending the data requires a rewind that failed.")
+    (66 . "Failed to initialise SSL Engine.")
+    (67 . "The user name, password, or similar was not accepted and curl 
failed to log in.")
+    (68 . "File not found on TFTP server.")
+    (69 . "Permission problem on TFTP server.")
+    (70 . "Out of disk space on TFTP server.")
+    (71 . "Illegal TFTP operation.")
+    (72 . "Unknown TFTP transfer ID.")
+    (73 . "File already exists (TFTP).")
+    (74 . "No such user (TFTP).")
+    (75 . "Character conversion failed.")
+    (76 . "Character conversion functions required.")
+    (77 . "Problem with reading the SSL CA cert (path? access rights?).")
+    (78 . "The resource referenced in the URL does not exist.")
+    (79 . "An unspecified error occurred during the SSH session.")
+    (80 . "Failed to shut down the SSL connection.")
+    (82 . "Could not load CRL file, missing or wrong format (added in 
7.19.0).")
+    (83 . "Issuer check failed (added in 7.19.0).")
+    (84 . "The FTP PRET command failed")
+    (85 . "RTSP: mismatch of CSeq numbers")
+    (86 . "RTSP: mismatch of Session Identifiers")
+    (87 . "unable to parse FTP file list")
+    (88 . "FTP chunk callback reported error")
+    (89 . "No connection available, the session will be queued")
+    (90 . "SSL public key does not matched pinned public key"))
+  "Alist mapping curl error code integers to helpful error messages.")
+
+;;;; Customization
+
+(defgroup plz nil
+  "Options for `plz'."
+  :group 'network
+  :link '(url-link "https://github.com/alphapapa/plz.el";))
+
+(defcustom plz-curl-program "curl"
+  "Name of curl program to call."
+  :type 'string)
+
+(defcustom plz-curl-default-args
+  '("--silent"
+    "--compressed"
+    "--location")
+  "Default arguments to curl.
+Note that these arguments are passed on the command line, which
+may be visible to other users on the local system."
+  :type '(repeat string))
+
+(defcustom plz-connect-timeout 5
+  "Default connection timeout in seconds.
+This limits how long the connection phase may last (the
+\"--connect-timeout\" argument to curl)."
+  :type 'number)
+
+(defcustom plz-timeout 60
+  "Default request timeout in seconds.
+This limits how long an entire request may take, including the
+connection phase and waiting to receive the response (the
+\"--max-time\" argument to curl)."
+  :type 'number)
+
+;;;; Functions
+
+;;;;; Public
+
+(cl-defun plz (method url &rest rest &key headers body else finally noquery 
process-filter
+                      (as 'string) (then 'sync)
+                      (body-type 'text) (decode t decode-s)
+                      (connect-timeout plz-connect-timeout) (timeout 
plz-timeout))
+  "Request METHOD from URL with curl.
+Return the curl process object or, for a synchronous request, the
+selected result.
+
+HEADERS may be an alist of extra headers to send with the
+request.
+
+BODY may be a string, a buffer, or a list like `(file FILENAME)'
+to upload a file from disk.
+
+BODY-TYPE may be `text' to send BODY as text, or `binary' to send
+it as binary.
+
+AS selects the kind of result to pass to the callback function
+THEN, or the kind of result to return for synchronous requests.
+It may be:
+
+- `buffer' to pass the response buffer, which will be narrowed to
+  the response body and decoded according to DECODE.
+
+- `binary' to pass the response body as an un-decoded string.
+
+- `string' to pass the response body as a decoded string.
+
+- `response' to pass a `plz-response' structure.
+
+- `file' to pass a temporary filename to which the response body
+  has been saved without decoding.
+
+- `(file FILENAME)' to pass FILENAME after having saved the
+  response body to it without decoding.  FILENAME must be a
+  non-existent file; if it exists, it will not be overwritten,
+  and an error will be signaled.
+
+- A function, which is called in the response buffer with it
+  narrowed to the response body (suitable for, e.g. `json-read').
+
+If DECODE is non-nil, the response body is decoded automatically.
+For binary content, it should be nil.  When AS is `binary',
+DECODE is automatically set to nil.
+
+THEN is a callback function, whose sole argument is selected
+above with AS; if the request fails and no ELSE function is
+given (see below), the argument will be a `plz-error' structure
+describing the error.  Or THEN may be `sync' to make a
+synchronous request, in which case the result is returned
+directly from this function.
+
+ELSE is an optional callback function called when the request
+fails (i.e. if curl fails, or if the HTTP response has a non-2xx
+status code).  It is called with one argument, a `plz-error'
+structure.  If ELSE is nil, a `plz-curl-error' or
+`plz-http-error' is signaled when the request fails, with a
+`plz-error' structure as the error data.  For synchronous
+requests, this argument is ignored.
+
+NOTE: In v0.8 of `plz', only one error will be signaled:
+`plz-error'.  The existing errors, `plz-curl-error' and
+`plz-http-error', inherit from `plz-error' to allow applications
+to update their code while using v0.7 (i.e. any `condition-case'
+forms should now handle only `plz-error', not the other two).
+
+FINALLY is an optional function called without argument after
+THEN or ELSE, as appropriate.  For synchronous requests, this
+argument is ignored.
+
+CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
+how long it takes to connect to a host and to receive a response
+from a host, respectively.
+
+NOQUERY is passed to `make-process', which see.
+
+\(To silence checkdoc, we mention the internal argument REST.)"
+  ;; FIXME(v0.8): Remove the note about error changes from the docstring.
+  ;; FIXME(v0.8): Update error signals in docstring.
+  (declare (indent defun))
+  (setf decode (if (and decode-s (not decode))
+                   nil decode))
+  ;; NOTE: By default, for PUT requests and POST requests >1KB, curl sends an
+  ;; "Expect:" header, which causes servers to send a "100 Continue" response, 
which
+  ;; we don't want to have to deal with, so we disable it by setting the 
header to
+  ;; the empty string.  See <https://gms.tf/when-curl-sends-100-continue.html>.
+  ;; TODO: Handle "100 Continue" responses and remove this workaround.
+  (push (cons "Expect" "") headers)
+  (let* ((data-arg (pcase-exhaustive body-type
+                     ('binary "--data-binary")
+                     ('text "--data")))
+         (curl-command-line-args (append plz-curl-default-args
+                                         (list "--config" "-")))
+         (curl-config-header-args (cl-loop for (key . value) in headers
+                                           collect (cons "--header" (format 
"%s: %s" key value))))
+         (curl-config-args (append curl-config-header-args
+                                   (list (cons "--url" url))
+                                   (when connect-timeout
+                                     (list (cons "--connect-timeout"
+                                                 (number-to-string 
connect-timeout))))
+                                   (when timeout
+                                     (list (cons "--max-time" 
(number-to-string timeout))))
+                                   ;; NOTE: To make a HEAD request
+                                   ;; requires using the "--head"
+                                   ;; option rather than "--request
+                                   ;; HEAD", and doing so with
+                                   ;; "--dump-header" duplicates the
+                                   ;; headers, so we must instead
+                                   ;; specify that for each other
+                                   ;; method.
+                                   (pcase method
+                                     ('get
+                                      (list (cons "--dump-header" "-")))
+                                     ((or 'put 'post)
+                                      (list (cons "--dump-header" "-")
+                                            (cons "--request" (upcase 
(symbol-name method)))
+                                            ;; It appears that this must be 
the last argument
+                                            ;; in order to pass data on the 
rest of STDIN.
+                                            (pcase body
+                                              (`(file ,filename)
+                                               ;; Use `expand-file-name' 
because curl doesn't
+                                               ;; expand, e.g. "~" into 
"/home/...".
+                                               (cons "--upload-file" 
(expand-file-name filename)))
+                                              (_ (cons data-arg "@-")))))
+                                     ('delete
+                                      (list (cons "--dump-header" "-")
+                                            (cons "--request" (upcase 
(symbol-name method)))))
+                                     ('head
+                                      (list (cons "--head" "")
+                                            (cons "--request" "HEAD"))))))
+         (curl-config (cl-loop for (key . value) in curl-config-args
+                               concat (format "%s \"%s\"\n" key value)))
+         (decode (pcase as
+                   ('binary nil)
+                   (_ decode)))
+         (default-directory
+          ;; Avoid making process in a nonexistent directory (in case the 
current
+          ;; default-directory has since been removed).  It's unclear what the 
best
+          ;; directory is, but this seems to make sense, and it should still 
exist.
+          temporary-file-directory)
+         (process-buffer (generate-new-buffer " *plz-request-curl*"))
+         (stderr-process (make-pipe-process :name "plz-request-curl-stderr"
+                                            :buffer (generate-new-buffer " 
*plz-request-curl-stderr*")
+                                            :noquery t
+                                            :sentinel #'plz--stderr-sentinel))
+         (process (make-process :name "plz-request-curl"
+                                :buffer process-buffer
+                                :coding 'binary
+                                :command (append (list plz-curl-program) 
curl-command-line-args)
+                                :connection-type 'pipe
+                                :filter process-filter
+                                :sentinel #'plz--sentinel
+                                :stderr stderr-process
+                                :noquery noquery))
+         sync-p)
+    (when (eq 'sync then)
+      (setf sync-p t
+            then (lambda (result)
+                   (process-put process :plz-result result))
+            else nil))
+    (setf
+     ;; Set the callbacks, etc. as process properties.
+     (process-get process :plz-then)
+     (pcase-exhaustive as
+       ((or 'binary 'string)
+        (lambda ()
+          (let ((coding-system (or (plz--coding-system) 'utf-8)))
+            (pcase as
+              ('binary (set-buffer-multibyte nil)))
+            (plz--narrow-to-body)
+            (when decode
+              (decode-coding-region (point) (point-max) coding-system))
+            (funcall then (or (buffer-string)
+                              (make-plz-error :message (format "buffer-string 
is nil in buffer:%S" process-buffer)))))))
+       ('buffer (progn
+                  (setf (process-get process :plz-as) 'buffer)
+                  (lambda ()
+                    (let ((coding-system (or (plz--coding-system) 'utf-8)))
+                      (pcase as
+                        ('binary (set-buffer-multibyte nil)))
+                      (plz--narrow-to-body)
+                      (when decode
+                        (decode-coding-region (point) (point-max) 
coding-system)))
+                    (funcall then (current-buffer)))))
+       ('response (lambda ()
+                    (funcall then (or (plz--response :decode-p decode :process 
process)
+                                      (make-plz-error :message (format 
"response is nil for buffer:%S  buffer-string:%S"
+                                                                       
process-buffer (buffer-string)))))))
+       ('file (lambda ()
+                (set-buffer-multibyte nil)
+                (plz--narrow-to-body)
+                (let ((filename (make-temp-file "plz-")))
+                  (condition-case err
+                      (progn
+                        (write-region (point-min) (point-max) filename)
+                        (funcall then filename))
+                    (file-already-exists
+                     (funcall then (make-plz-error :message (format "error 
while writing to file %S: %S" filename err))))
+                    ;; In case of an error writing to the file, delete the 
temp file
+                    ;; and signal the error.  Ignore any errors encountered 
while
+                    ;; deleting the file, which would obscure the original 
error.
+                    (error (ignore-errors
+                             (delete-file filename))
+                           (funcall then (make-plz-error :message (format 
"error while writing to file %S: %S" filename err))))))))
+       (`(file ,(and (pred stringp) filename))
+        (lambda ()
+          (set-buffer-multibyte nil)
+          (plz--narrow-to-body)
+          (condition-case err
+              (progn
+                (write-region (point-min) (point-max) filename nil nil nil 
'excl)
+                (funcall then filename))
+            (file-already-exists
+             (funcall then (make-plz-error :message (format "error while 
writing to file %S: %S" filename err))))
+            ;; Since we are creating the file, it seems sensible to delete it 
in case of an
+            ;; error while writing to it (e.g. a disk-full error).  And we 
ignore any errors
+            ;; encountered while deleting the file, which would obscure the 
original error.
+            (error (ignore-errors
+                     (when (file-exists-p filename)
+                       (delete-file filename)))
+                   (funcall then (make-plz-error :message (format "error while 
writing to file %S: %S" filename err)))))))
+       ((pred functionp) (lambda ()
+                           (let ((coding-system (or (plz--coding-system) 
'utf-8)))
+                             (plz--narrow-to-body)
+                             (when decode
+                               (decode-coding-region (point) (point-max) 
coding-system))
+                             (funcall then (funcall as))))))
+     (process-get process :plz-else) else
+     (process-get process :plz-finally) finally
+     (process-get process :plz-sync) sync-p
+     ;; Record list of arguments for debugging purposes (e.g. when
+     ;; using Edebug in a process buffer, this allows determining
+     ;; which request the buffer is for).
+     (process-get process :plz-args) (apply #'list method url rest)
+     ;; HACK: We set the result to a sentinel value so that any other
+     ;; value, even nil, means that the response was processed, and
+     ;; the sentinel does not need to be called again (see below).
+     (process-get process :plz-result) :plz-result)
+    ;; Send --config arguments.
+    (process-send-string process curl-config)
+    (when body
+      (cl-typecase body
+        (string (process-send-string process body))
+        (buffer (with-current-buffer body
+                  (process-send-region process (point-min) (point-max))))))
+    (process-send-eof process)
+    (if sync-p
+        (unwind-protect
+            (with-local-quit
+              ;; See Info node `(elisp)Accepting Output'.
+              (unless (and process stderr-process)
+                (error "Process unexpectedly nil"))
+              (while (accept-process-output process))
+              (while (accept-process-output stderr-process))
+              (when (eq :plz-result (process-get process :plz-result))
+                ;; HACK: Sentinel seems to not have been called: call it 
again.  (Although
+                ;; this is a hack, it seems to be a necessary one due to 
Emacs's process
+                ;; handling.)  See 
<https://github.com/alphapapa/plz.el/issues/3> and
+                ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50166>.
+                (plz--sentinel process "finished\n")
+                (when (eq :plz-result (process-get process :plz-result))
+                  (error "Plz: NO RESULT FROM PROCESS:%S  ARGS:%S"
+                         process rest)))
+              ;; Sentinel seems to have been called: check the result.
+              (pcase (process-get process :plz-result)
+                ((and (pred plz-error-p) data)
+                 ;; The AS function signaled an error, which was collected
+                 ;; into a `plz-error' struct: re-signal the error here,
+                 ;; outside of the sentinel.
+                 (if (plz-error-response data)
+                     ;; FIXME(v0.8): Signal only plz-error.
+                     (signal 'plz-http-error (list "HTTP error" data))
+                   (signal 'plz-curl-error (list "Curl error" data))))
+                (else
+                 ;; The AS function returned a value: return it.
+                 else)))
+          (unless (eq as 'buffer)
+            (kill-buffer process-buffer))
+          (kill-buffer (process-buffer stderr-process)))
+      ;; Async request: return the process object.
+      process)))
+
+;;;;; Queue
+
+;; A simple queue system.
+
+(cl-defstruct plz-queued-request
+  "Structure representing a queued `plz' HTTP request.
+For more details on these slots, see arguments to the function
+`plz'."
+  method url headers body else finally noquery
+  as then body-type decode
+  connect-timeout timeout
+  next previous process)
+
+(cl-defstruct plz-queue
+  "Structure forming a queue for `plz' requests.
+The queue may be appended to (the default) and pre-pended to, and
+items may be removed from the front of the queue (i.e. by
+default, it's FIFO).  Use functions `plz-queue', `plz-run', and
+`plz-clear' to queue, run, and clear requests, respectively."
+  (limit 1
+         :documentation "Number of simultaneous requests.")
+  (active nil
+          :documentation "Active requests.")
+  (requests nil
+            :documentation "Queued requests.")
+  (canceled-p nil
+              :documentation "Non-nil when queue has been canceled.")
+  first-active last-active
+  first-request last-request
+  (finally nil
+           :documentation "Function called with no arguments after queue has 
been emptied or canceled."))
+
+(defun plz-queue (queue &rest args)
+  "Queue request for ARGS on QUEUE and return QUEUE.
+To pre-pend to QUEUE rather than append, it may be a list of the
+form (`prepend' QUEUE).  QUEUE is a `plz-request' queue.  ARGS
+are those passed to `plz', which see.  Use `plz-run' to start
+making QUEUE's requests."
+  (declare (indent defun))
+  (cl-assert (not (equal 'sync (plist-get (cddr args) :then))) nil
+             "Only async requests may be queued")
+  (pcase-let* ((`(,method ,url . ,rest) args)
+               (args `(:method ,method :url ,url ,@rest))
+               (request (apply #'make-plz-queued-request args)))
+    (pcase queue
+      (`(prepend ,queue) (plz--queue-prepend request queue))
+      (_ (plz--queue-append request queue))))
+  queue)
+
+(defun plz--queue-append (request queue)
+  "Add REQUEST to end of QUEUE and return QUEUE."
+  (cl-check-type request plz-queued-request
+                 "REQUEST must be a `plz-queued-request' structure.")
+  (cl-check-type queue plz-queue
+                 "QUEUE must be a `plz-queue' structure.")
+  (when (plz-queue-last-request queue)
+    (setf (plz-queued-request-next (plz-queue-last-request queue)) request))
+  (setf (plz-queued-request-previous request) (plz-queue-last-request queue)
+        (plz-queue-last-request queue) request)
+  (unless (plz-queue-first-request queue)
+    (setf (plz-queue-first-request queue) request))
+  (unless (plz-queue-last-request queue)
+    (setf (plz-queue-last-request queue) request))
+  (push request (plz-queue-requests queue))
+  queue)
+
+(defun plz--queue-prepend (request queue)
+  "Add REQUEST to front of QUEUE and return QUEUE."
+  (cl-check-type request plz-queued-request
+                 "REQUEST must be a `plz-queued-request' structure.")
+  (cl-check-type queue plz-queue
+                 "QUEUE must be a `plz-queue' structure.")
+  (when (plz-queue-requests queue)
+    (setf (plz-queued-request-next request) (car (plz-queue-requests queue))
+          (plz-queued-request-previous (plz-queued-request-next request)) 
request))
+  (setf (plz-queue-first-request queue) request)
+  (unless (plz-queue-first-request queue)
+    (setf (plz-queue-first-request queue) request))
+  (unless (plz-queue-last-request queue)
+    (setf (plz-queue-last-request queue) request))
+  (push request (plz-queue-requests queue))
+  queue)
+
+(defun plz--queue-pop (queue)
+  "Return the first queued request on QUEUE and remove it from QUEUE."
+  (let* ((request (plz-queue-first-request queue))
+         (next (plz-queued-request-next request)))
+    (when next
+      (setf (plz-queued-request-previous next) nil))
+    (setf (plz-queue-first-request queue) next
+          (plz-queue-requests queue) (delq request (plz-queue-requests queue)))
+    (when (eq request (plz-queue-last-request queue))
+      (setf (plz-queue-last-request queue) nil))
+    request))
+
+(defun plz-run (queue)
+  "Process requests in QUEUE and return QUEUE.
+Return when QUEUE is at limit or has no more queued requests.
+
+QUEUE should be a `plz-queue' structure."
+  (cl-labels ((readyp (queue)
+                (and (not (plz-queue-canceled-p queue))
+                     (plz-queue-requests queue)
+                     ;; With apologies to skeeto...
+                     (< (length (plz-queue-active queue)) (plz-queue-limit 
queue)))))
+    (while (readyp queue)
+      (pcase-let* ((request (plz--queue-pop queue))
+                   ((cl-struct plz-queued-request method url
+                               headers body finally noquery as body-type 
decode connect-timeout timeout
+                               (else orig-else) (then orig-then))
+                    request)
+                   (then (lambda (response)
+                           (unwind-protect
+                               ;; Ensure any errors in the THEN function don't 
abort the queue.
+                               (funcall orig-then response)
+                             (setf (plz-queue-active queue) (delq request 
(plz-queue-active queue)))
+                             (plz-run queue))))
+                   (else (lambda (arg)
+                           ;; FIXME(v0.8): This should be done in `plz-queue' 
because
+                           ;; `plz-clear' will call the second 
queued-request's ELSE
+                           ;; before it can be set by `plz-run'.
+                           (unwind-protect
+                               ;; Ensure any errors in the THEN function don't 
abort the queue.
+                               (when orig-else
+                                 (funcall orig-else arg))
+                             (setf (plz-queue-active queue) (delq request 
(plz-queue-active queue)))
+                             (plz-run queue))))
+                   (args (list method url
+                               ;; Omit arguments for which `plz' has defaults 
so as not to nil them.
+                               :headers headers :body body :finally finally 
:noquery noquery
+                               :connect-timeout connect-timeout :timeout 
timeout)))
+        ;; Add arguments which override defaults.
+        (when as
+          (setf args (plist-put args :as as)))
+        (when else
+          (setf args (plist-put args :else else)))
+        (when then
+          (setf args (plist-put args :then then)))
+        (when decode
+          (setf args (plist-put args :decode decode)))
+        (when body-type
+          (setf args (plist-put args :body-type body-type)))
+        (when connect-timeout
+          (setf args (plist-put args :connect-timeout connect-timeout)))
+        (when timeout
+          (setf args (plist-put args :timeout timeout)))
+        (setf (plz-queued-request-process request) (apply #'plz args))
+        (push request (plz-queue-active queue))))
+    (when (and (plz-queue-finally queue)
+               (zerop (length (plz-queue-active queue)))
+               (zerop (length (plz-queue-requests queue))))
+      (funcall (plz-queue-finally queue)))
+    queue))
+
+(defun plz-clear (queue)
+  "Clear QUEUE and return it.
+Cancels any active or pending requests and calls the queue's
+FINALLY function.  For pending requests, their ELSE functions
+will be called with a `plz-error' structure with the message,
+\"`plz' queue cleared; request canceled.\"; active requests will
+have their curl processes killed and their ELSE functions called
+with the corresponding data."
+  (setf (plz-queue-canceled-p queue) t)
+  (dolist (request (plz-queue-active queue))
+    (when (process-live-p (plz-queued-request-process request))
+      (kill-process (plz-queued-request-process request)))
+    (setf (plz-queue-active queue) (delq request (plz-queue-active queue))))
+  (dolist (request (plz-queue-requests queue))
+    (funcall (plz-queued-request-else request)
+             (make-plz-error :message "`plz' queue cleared; request 
canceled."))
+    (setf (plz-queue-requests queue) (delq request (plz-queue-requests 
queue))))
+  (when (plz-queue-finally queue)
+    (funcall (plz-queue-finally queue)))
+  (setf (plz-queue-first-active queue) nil
+        (plz-queue-last-active queue) nil
+        (plz-queue-first-request queue) nil
+        (plz-queue-last-request queue) nil
+        (plz-queue-canceled-p queue) nil)
+  queue)
+
+(defun plz-length (queue)
+  "Return number of of QUEUE's outstanding requests.
+Includes active and queued requests."
+  (+ (length (plz-queue-active queue))
+     (length (plz-queue-requests queue))))
+
+;;;;; Private
+
+(defun plz--sentinel (process status)
+  "Sentinel for curl PROCESS.
+STATUS should be the process's event string (see info
+node `(elisp) Sentinels').  Calls `plz--respond' to process the
+HTTP response (directly for synchronous requests, or from a timer
+for asynchronous ones)."
+  (pcase status
+    ((or "finished\n" "killed\n" "interrupt\n"
+         (pred numberp)
+         (rx "exited abnormally with code " (group (1+ digit))))
+     (let ((buffer (process-buffer process)))
+       (if (process-get process :plz-sync)
+           (plz--respond process buffer status)
+         (run-at-time 0 nil #'plz--respond process buffer status))))))
+
+(defun plz--respond (process buffer status)
+  "Respond to HTTP response from PROCESS in BUFFER.
+Parses the response and calls the THEN/ELSE callbacks
+accordingly.  To be called from `plz--sentinel'.  STATUS is the
+argument passed to `plz--sentinel', which see."
+  ;; Is it silly to call this function "please respond"?  Perhaps, but
+  ;; naming things is hard.  The term "process" has another meaning in
+  ;; this context, and the old standby, "handle," is much overused.
+  ;; "Respond" also means "to react to something," which is what this
+  ;; does--react to receiving the HTTP response--and it's an internal
+  ;; name, so why not.
+  (unwind-protect
+      (with-current-buffer buffer
+        (pcase-exhaustive status
+          ((or 0 "finished\n")
+           ;; Curl exited normally: check HTTP status code.
+           (goto-char (point-min))
+           (plz--skip-proxy-headers)
+           (while (plz--skip-redirect-headers))
+           (pcase (plz--http-status)
+             ((and status (guard (<= 200 status 299)))
+              ;; Any 2xx response is considered successful.
+              (ignore status) ; Byte-compiling in Emacs <28 complains without 
this.
+              (funcall (process-get process :plz-then)))
+             (_
+              ;; TODO: If using ":as 'response", the HTTP response
+              ;; should be passed to the THEN function, regardless
+              ;; of the status code.  Only for curl errors should
+              ;; the ELSE function be called.  (Maybe in v0.8.)
+
+              ;; Any other status code is considered unsuccessful
+              ;; (for now, anyway).
+              (let ((err (make-plz-error :response (plz--response :process 
process))))
+                (pcase-exhaustive (process-get process :plz-else)
+                  (`nil (process-put process :plz-result err))
+                  ((and (pred functionp) fn) (funcall fn err)))))))
+
+          ((or (and (pred numberp) code)
+               (rx "exited abnormally with code " (let code (group (1+ 
digit)))))
+           ;; Curl error.
+           (let* ((curl-exit-code (cl-typecase code
+                                    (string (string-to-number code))
+                                    (number code)))
+                  (curl-error-message (alist-get curl-exit-code 
plz-curl-errors))
+                  (err (make-plz-error :curl-error (cons curl-exit-code 
curl-error-message))))
+             (pcase-exhaustive (process-get process :plz-else)
+               (`nil (process-put process :plz-result err))
+               ((and (pred functionp) fn) (funcall fn err)))))
+
+          ((and (or "killed\n" "interrupt\n") status)
+           ;; Curl process killed or interrupted.
+           (let* ((message (pcase status
+                             ("killed\n" "curl process killed")
+                             ("interrupt\n" "curl process interrupted")))
+                  (err (make-plz-error :message message)))
+             (pcase-exhaustive (process-get process :plz-else)
+               (`nil (process-put process :plz-result err))
+               ((and (pred functionp) fn) (funcall fn err)))))))
+    (when-let ((finally (process-get process :plz-finally)))
+      (funcall finally))
+    (unless (or (process-get process :plz-sync)
+                (eq 'buffer (process-get process :plz-as)))
+      (kill-buffer buffer))))
+
+(defun plz--stderr-sentinel (process status)
+  "Sentinel for STDERR buffer.
+Arguments are PROCESS and STATUS (ok, checkdoc?)."
+  (pcase status
+    ((or "finished\n" "killed\n" "interrupt\n"
+         (pred numberp)
+         (rx "exited abnormally with code " (1+ digit)))
+     (kill-buffer (process-buffer process)))))
+
+;;;;;; HTTP Responses
+
+;; Functions for parsing HTTP responses.
+
+(defun plz--skip-proxy-headers ()
+  "Skip proxy headers in current buffer."
+  (when (looking-at plz-http-response-status-line-regexp)
+    (let* ((status-code (string-to-number (match-string 2)))
+           (reason-phrase (match-string 3)))
+      (when (and (equal 200 status-code)
+                 (equal "Connection established" reason-phrase))
+        ;; Skip proxy headers (curl apparently offers no way to omit
+        ;; them).
+        (unless (re-search-forward "\r\n\r\n" nil t)
+          (signal 'plz-http-error '("plz--response: End of proxy headers not 
found")))))))
+
+(defun plz--skip-redirect-headers ()
+  "Skip HTTP redirect headers in current buffer."
+  (when (and (looking-at plz-http-response-status-line-regexp)
+             (member (string-to-number (match-string 2)) '(301 302 303 307 
308)))
+    ;; Skip redirect headers ("--dump-header" forces redirect headers to be 
included
+    ;; even when used with "--location").
+    (or (re-search-forward "\r\n\r\n" nil t)
+        (signal 'plz-http-error '("plz--response: End of redirect headers not 
found")))))
+
+(cl-defun plz--response (&key (decode-p t) process)
+  "Return response structure for HTTP response in current buffer.
+When DECODE-P is non-nil, decode the response body automatically
+according to the apparent coding system.
+
+PROCESS is the curl process object that made the request.
+
+Assumes that point is at beginning of HTTP response."
+  (save-excursion
+    ;; Parse HTTP version and status code.
+    (unless (looking-at plz-http-response-status-line-regexp)
+      (signal 'plz-http-error
+              (list "plz--response: Unable to parse HTTP response status line"
+                    (buffer-substring (point) (line-end-position)))))
+    (let* ((http-version (string-to-number (match-string 1)))
+           (status-code (string-to-number (match-string 2)))
+           (headers (plz--headers))
+           (coding-system (or (plz--coding-system headers) 'utf-8)))
+      (plz--narrow-to-body)
+      (when decode-p
+        (decode-coding-region (point) (point-max) coding-system))
+      (make-plz-response
+       :version http-version
+       :status status-code
+       :headers headers
+       :body (buffer-string)
+       :process process))))
+
+(defun plz--coding-system (&optional headers)
+  "Return coding system for HTTP response in current buffer.
+HEADERS may optionally be an alist of parsed HTTP headers to
+refer to rather than the current buffer's un-parsed headers."
+  (let* ((headers (or headers (plz--headers)))
+         (content-type (alist-get 'content-type headers)))
+    (when content-type
+      (coding-system-from-name content-type))))
+
+(defun plz--http-status ()
+  "Return HTTP status code for HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+  (when (looking-at plz-http-response-status-line-regexp)
+    (string-to-number (match-string 2))))
+
+(defun plz--headers ()
+  "Return headers alist for HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+  (save-excursion
+    (forward-line 1)
+    (let ((limit (save-excursion
+                   (re-search-forward plz-http-end-of-headers-regexp nil)
+                   (point))))
+      (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":")))) 
":" (1+ blank)
+                                            (group (1+ (not (in "\r\n")))))
+                                        limit t)
+               ;; NOTE: Some HTTP servers send all-lowercase header keys, 
which means an alist
+               ;; lookup with `equal' or `string=' fails when the case 
differs.  We don't want
+               ;; users to have to worry about this, so for consistency, we 
downcase the
+               ;; header name.  And while we're at it, we might as well intern 
it so we can
+               ;; use `alist-get' without having to add "nil nil #'equal" 
every time.
+               collect (cons (intern (downcase (match-string 1))) 
(match-string 2))))))
+
+(defun plz--narrow-to-body ()
+  "Narrow to body of HTTP response in current buffer.
+Assumes point is at start of HTTP response."
+  (unless (re-search-forward plz-http-end-of-headers-regexp nil t)
+    (signal 'plz-http-error '("plz--narrow-to-body: Unable to find end of 
headers")))
+  (narrow-to-region (point) (point-max)))
+
+;;;; Footer
+
+(provide 'plz)
+
+;;; plz.el ends here



reply via email to

[Prev in Thread] Current Thread [Next in Thread]