Function: plz

plz is a natively compiled function defined in plz.el.

Signature

(plz METHOD URL &rest REST &key HEADERS BODY ELSE FILTER FINALLY NOQUERY TIMEOUT (AS 'string) (THEN 'sync) (BODY-TYPE 'text) (DECODE t DECODE-S) (CONNECT-TIMEOUT plz-connect-timeout))

Documentation

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. FILENAME is passed through
  expand-file-name, which see.

- 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 a future version 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 earlier versions (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 complete response from a host, respectively.

NOQUERY is passed to make-process, which see.

FILTER is an optional function to be used as the process filter for the curl process. It can be used to handle HTTP responses in a streaming way. The function must accept 2 arguments, the process object running curl, and a string which is output received from the process. The default process filter inserts the output of the process into the process buffer. The provided FILTER function should at least insert output up to the HTTP body into the process buffer.

(To silence checkdoc, we mention the internal argument REST.)

Source Code

;; Defined in /nix/store/jagqw3nms546pad0gxi5f3wsckk6pji7-emacs-packages-deps/share/emacs/site-lisp/elpa/plz-0.9.1/plz.el
;;;; Functions

;;;;; Public

(cl-defun plz (method url &rest rest &key headers body else filter finally noquery timeout
                      (as 'string) (then 'sync)
                      (body-type 'text) (decode t decode-s)
                      (connect-timeout plz-connect-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.  FILENAME is passed through
  `expand-file-name', which see.

- 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 a future version 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 earlier versions (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 complete
response from a host, respectively.

NOQUERY is passed to `make-process', which see.

FILTER is an optional function to be used as the process filter
for the curl process.  It can be used to handle HTTP responses in
a streaming way.  The function must accept 2 arguments, the
process object running curl, and a string which is output
received from the process.  The default process filter inserts
the output of the process into the process buffer.  The provided
FILTER function should at least insert output up to the HTTP body
into the process buffer.

\(To silence checkdoc, we mention the internal argument REST.)"
  ;; FIXME(v0.10): Remove the note about error changes from the docstring.
  ;; FIXME(v0.10): 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* (filename
         (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
                                      (append (list (cons "--dump-header" "-"))
                                              (pcase as
                                                ('file
                                                 (setf filename (make-temp-file "plz-"))
                                                 (list (cons "--output" filename)))
                                                (`(file ,(and (pred stringp) as-filename))
                                                 (when (file-exists-p as-filename)
                                                   (error "File exists, will not overwrite: %S" as-filename))
                                                 ;; Use `expand-file-name' because curl doesn't
                                                 ;; expand, e.g. "~" into "/home/...".
                                                 (setf filename (expand-file-name as-filename))
                                                 (list (cons "--output" filename))))))
                                     ((or 'put 'post)
                                      (append (list (cons "--dump-header" "-")
                                                    (cons "--request" (upcase (symbol-name method))))
                                              (pcase as
                                                ('file
                                                 (setf filename (make-temp-file "plz-"))
                                                 (list (cons "--output" filename)))
                                                (`(file ,(and (pred stringp) as-filename))
                                                 (when (file-exists-p as-filename)
                                                   (error "File exists, will not overwrite: %S" as-filename))
                                                 ;; Use `expand-file-name' because curl doesn't
                                                 ;; expand, e.g. "~" into "/home/...".
                                                 (setf filename (expand-file-name as-filename))
                                                 (list (cons "--output" filename))))
                                              (list
                                               ;; 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
                                      (append (list (cons "--dump-header" "-")
                                                    (cons "--request" (upcase (symbol-name method))))
                                              (pcase as
                                                ('file
                                                 (setf filename (make-temp-file "plz-"))
                                                 (list (cons "--output" filename)))
                                                (`(file ,(and (pred stringp) as-filename))
                                                 (when (file-exists-p as-filename)
                                                   (error "File exists, will not overwrite: %S" as-filename))
                                                 ;; Use `expand-file-name' because curl doesn't
                                                 ;; expand, e.g. "~" into "/home/...".
                                                 (setf filename (expand-file-name as-filename))
                                                 (list (cons "--output" filename))))))
                                     ('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 (plz--generate-new-buffer " *plz-request-curl*" t))
         (stderr-process (make-pipe-process :name "plz-request-curl-stderr"
                                            :buffer (plz--generate-new-buffer " *plz-request-curl-stderr*" t)
                                            :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 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)
                                      (make-plz-error :message (format "response is nil for buffer:%S  buffer-string:%S"
                                                                       process-buffer (buffer-string)))))))
       ('file (lambda ()
                (funcall then filename)))
       (`(file ,(and (pred stringp) filename))
        ;; This requires a separate clause due to the FILENAME binding.
        (lambda ()
          (funcall then filename)))
       ((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))
              (plz-debug (float-time) "BEFORE HACK" (process-buffer process))
              (when (eq :plz-result (process-get process :plz-result))
                (plz-debug (float-time) "INSIDE HACK" (process-buffer process))
                ;; 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 "workaround")
                (plz-debug (float-time) "INSIDE HACK, AFTER CALLING SENTINEL" (process-buffer process))
                (when (eq :plz-result (process-get process :plz-result))
                  (error "Plz: NO RESULT FROM PROCESS:%S  ARGS:%S"
                         process rest)))
              (plz-debug (float-time) "AFTER HACK" (process-buffer process))
              ;; 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.10): 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)
            (plz--kill-buffer process-buffer))
          (plz--kill-buffer (process-buffer stderr-process)))
      ;; Async request: return the process object.
      process)))