{"id":13646231,"url":"https://github.com/akamai/cl-http2-protocol","last_synced_at":"2025-08-03T04:33:19.066Z","repository":{"id":15789071,"uuid":"18528297","full_name":"akamai/cl-http2-protocol","owner":"akamai","description":"HTTP/2 interop library in Common Lisp","archived":true,"fork":false,"pushed_at":"2014-10-17T02:41:40.000Z","size":636,"stargazers_count":106,"open_issues_count":2,"forks_count":8,"subscribers_count":25,"default_branch":"master","last_synced_at":"2025-08-03T04:32:57.333Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/akamai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-04-07T18:30:36.000Z","updated_at":"2025-06-06T16:37:27.000Z","dependencies_parsed_at":"2022-07-16T07:30:28.639Z","dependency_job_id":null,"html_url":"https://github.com/akamai/cl-http2-protocol","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/akamai/cl-http2-protocol","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akamai%2Fcl-http2-protocol","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akamai%2Fcl-http2-protocol/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akamai%2Fcl-http2-protocol/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akamai%2Fcl-http2-protocol/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akamai","download_url":"https://codeload.github.com/akamai/cl-http2-protocol/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akamai%2Fcl-http2-protocol/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268495890,"owners_count":24259397,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-03T02:00:12.545Z","response_time":2577,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T01:02:51.057Z","updated_at":"2025-08-03T04:33:19.038Z","avatar_url":"https://github.com/akamai.png","language":"Common Lisp","funding_links":[],"categories":["Common Lisp","Interfaces to other package managers"],"sub_categories":[],"readme":"CL-HTTP2-PROTOCOL\n========\n\nWhat This Is\n------------\n\nThis is\n[HTTP/2 draft-14](http://tools.ietf.org/html/draft-ietf-httpbis-http2-14)\n(a.k.a. \"h2-14\")\n[interopability test code](https://github.com/http2/http2-spec/wiki/Implementations)\nwritten in Common Lisp. It has only been tested against SBCL 1.1.8.0\nto 1.1.14 on Ubuntu Linux on x86, but it should be possible to make it\nwork on other Common Lisp implementations, subject to some editing in\n`util.lisp` and possibly `example.lisp`.\n\nThe code offers a pure Common Lisp transport agnostic implementation\nof the HTTP/2 protocol at draft-14. An example client and server are\nincluded for a \"Hello, World\" style test, which employ TLS using\n`CL+SSL` and OpenSSL.\n\nNetworking examples have been based on the `CL-ASYNC` library. Some\nalterations to functions in `CL+SSL` and `CL-ASYNC` were necessary and\nare included in the source tree. (Prior versions of this library were\nbased on homemade event loops based on `USOCKET` and `SB-BSD-SOCKETS`\nbut this has now been removed.)\n\nThe current implementation is based on:\n\n* [draft-ietf-httpbis-http2-14](http://tools.ietf.org/html/draft-ietf-httpbis-http2-14)\n* [draft-ietf-httpbis-header-compression-09](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-09)\n\nSupport for:\n\n* [Binary\n  framing](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#_binary_framing_layer)\n  parsing and encoding\n* [Stream\n  multiplexing](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#HTTP2_STREAMS_MESSAGES_FRAMES)\n  and\n  [prioritization](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#HTTP2_PRIORITIZATION)\n* Connection and stream [flow\n  control](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#_flow_control)\n* [Header\n  compression](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#HTTP2_HEADER_COMPRESSION)\n* [Server push](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#HTTP2_PUSH)\n* Connection and stream management\n\nCopyright\n---------\n\n* Copyright (c) 2014 Akamai Technologies, Inc. Published under MIT\n  License.\n\n* Contains design and text from\n  [http-2](https://github.com/igrigorik/http-2) which contains this\n  notice: \"(MIT License) - Copyright (c) 2013 Ilya Grigorik\"\n\n* Contains code from\n  [CL+SSL](http://common-lisp.net/project/cl-plus-ssl/) which contains\n  this notice: \"This library is a fork of SSL-CMUCL. The original\n  SSL-CMUCL source code was written by Eric Marsden and includes\n  contributions by Jochen Schmidt. Development into CL+SSL was done by\n  David Lichteblau. License: MIT-style.\"\n\n* Contains code from\n  [CL-ASYNC](https://github.com/orthecreedence/cl-async) which contains\n  this notice: \"As always, my code is MIT licenced. Do whatever the\n  hell you want with it. Enjoy!\"\n\nNotes on Port\n-------------\n\nThis code began life as a port from the draft-06\n[Ruby interopability code](https://github.com/igrigorik/http-2)\nwritten by Ilya Grigorik released under MIT license. This code has\nsince diverged to support subsequent working group drafts (up to\ndraft-14 currently), to allow queueing and multiplexing\nsupport with a prioritization algorithm, and to move away from some\nRuby idioms towards Lisp idioms over time.\n\nThe following are notes only of interest to folks who followed along\nsince the beginning or have some interest in comparing the code\nrevisions. Others may skip this section.\n\n* `util.lisp` defines several general purpose forms that give us some\n  capabilities similar to calls in Ruby, as well as convenience calls\n  that are stylistic Lisp choices.\n\n* `buffer.lisp` is much longer than `buffer.rb` in order to allow us\n  to build up various primitives that Ruby offers in the `String`\n  class for free.\n\n* `ssl.lisp` redefines some items in the `CL+SSL` package (based on\n  the `cl+ssl-20140316-git` version) and `tcp-ssl.lisp` overrides some\n  items in the `CL-ASYNC-SSL` package (based on the\n  `cl-async-20140616-git` version), mostly to add support for\n  [NPN](https://technotes.googlecode.com/git/nextprotoneg.html),\n  [SNI](http://en.wikipedia.org/wiki/Server_Name_Indication), and\n  [DH parameters](http://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange),\n  all of which are neecessary to establish the TLS connection as\n  required by HTTP/2. See the comments in those files for more\n  information.\n\n* `tcp.lisp` redefines some items in the `CL-ASYNC` package (based on\n  the `cl-async-20140616-git` version) mostly to fix a couple issues\n  encountered in corner cases.\n\n* The code in the `example` folder is contained in `example.lisp` in\n  the form of functions. The examples in Lisp fire up `CL-ASYNC` event\n  loops.\n  \n* The Ruby code uses arrays and hashes which in the CL code are\n  variously ported as alists, plists, and hashes depending on the\n  specifics of access required.\n\n* The error conditions are largely the same, but are prefixed with\n  `HTTP2-` and an additional one is added named `HTTP2-NOT-STARTED` as\n  it is very convenient in debugging (when NPN fails, etc). In\n  addition, the Lisp code defines various restarts in the normal\n  manner, so that during debugging certain failures do not require the\n  instant transaction to be aborted entirely.\n\n* Classes are matched one-to-one but module/package organization is\n  different. Use the `HTTP2` package for most functions and the\n  `HTTP2-EXAMPLE` package for the examples.\n\nThis Common Lisp code was produced by Martin Flack, a Principal\nArchitect on the Foundry team in the Web Experience business at\n[Akamai](http://www.akamai.com/).\n\nOur team's mission is innovative applied R\u0026D, and accordingly we\nexplore new technologies close to the mission of excellent web\nexperience. Note that this code is intended to be used against the\nother HTTP/2 interopability client/server code linked above, and not\nnecessarily any part of the Akamai network; nor is any feature herein\nindicative of any planned or actual feature of Akamai products or\nservices.\n\nThis system was named `CL-HTTP2-PROTOCOL` to avoid misunderstanding\nthat it was related to `CL-HTTP`, a Common Lisp web server.\nApologies for the redundant word.\n\nServer Setup and Example\n------------\n\nTo run this on a fresh Ubuntu Linux 13.10 or 14.04 server, follow these\ninstructions. A non-root user of \"ubuntu\" with sudo access is assumed.\n\n```shell\nsudo apt-get update \u0026\u0026 sudo apt-get dist-upgrade -y \u0026\u0026 sudo reboot\n# ...if prompted about grub choose \"install package maintainer's version\"\n#\n# ...login again\nsudo apt-get install -y git sbcl\ngit clone https://github.com/akamai/cl-http2-protocol.git\nwget http://beta.quicklisp.org/quicklisp.lisp\nsbcl --script \u003c\u003cEOF\n(load \"quicklisp.lisp\")\n(quicklisp-quickstart:install)\n(ql:quickload :swank)\n(ql:quickload :alexandria)\n(ql:quickload :anaphora)\n(ql:quickload :babel)\n(ql:quickload :puri)\n(ql:quickload :usocket)\n(ql:quickload :cl+ssl)\n(ql:quickload :cl-async)\n(ql:quickload :cl-async-ssl)\nEOF\n#\n# ...optionally run screen if you're familiar with it and you want to\n# quickly test server/client on one system:\nscreen\n#\n# ...start the server:\nsbcl --script \u003c\u003cEOF\n(load \"quicklisp/setup.lisp\")\n(load \"cl-http2-protocol/cl-http2-protocol.asd\")\n(require :cl-http2-protocol)\n(in-package :http2-example)\n(example-server)\nEOF\n#\n# ...now you have an HTTP/2 server, secured with TLS, on port 8080\n# (note the server is ONLY HTTP/2; there is no HTTP/1.1 fallback)\n# (note that port 8080 is chosen only to allow a non-root user to\n# run the Lisp image and launch the server)\n#\n# ...to stop the server, CTRL+C followed by ABORT at the handler\n#\n# ...if you prefer to have less output and keep exceptions from\n# stopping the program, break the server, set these globals and rerun\n# the server:\n(setf *verbose-mode* nil)\n(setf *debug-mode* nil)\n(setf *dump-bytes* nil)\n(example-server)\n#\n# ...if you ran screen, press CTRL-A CTRL-C, else open a new terminal\n# to the server in order to run the example client\n#\n# ...run a client:\nsbcl --script \u003c\u003cEOF\n(load \"quicklisp/setup.lisp\")\n(load \"cl-http2-protocol/cl-http2-protocol.asd\")\n(require :cl-http2-protocol)\n(in-package :http2-example)\n(example-client \"https://localhost:8080/\")\nEOF\n```\n\nPlease note that the files `mykey.pem`, `mycert.pem`, and\n`dhparams.2048.pem` are used for the example server, so it is\nrecommended that you regenerate them. You will then remove security\nwarnings from real browsers. Please note that `EXAMPLE-CLIENT` does\nabsolutely no sensible checking of the TLS certificate, etc.\n\n```shell\nopenssl genrsa -out mykey.pem 2048\nopenssl req -new -key mykey.pem -out mycert.csr\n# CN should be set to the hostname you will use to reach the server\nopenssl req -noout -text -in mycert.csr\nopenssl x509 -req -days 365 -in mycert.csr -signkey mykey.pem -out mycert.pem\nopenssl dhparam -outform pem -out dhparams.2048.pem 2048\n```\n\nAt the time of writing (Sep 10, 2014), Firefox Nightly and Chrome\nCanary will be compatible with your HTTP/2 server started above as\nwell. The address bar in Firefox Nightly should be\n`https://HOST:8080/` where HOST is the hostname used to reach your\nserver.\n\nGetting Started\n---------------\n\n```lisp\n(load \"cl-http2-protocol/cl-http2-protocol.asd\")\n(require :cl-http2-protocol)\n\n(defvar socket #|  provide a transport...  |#)\n\n(defvar conn (make-instance 'client))\n(on conn :frame (lambda (bytes) #|  send the bytes...  |#))\n\n(loop for bytes = #|  read some bytes...  |#\n      if bytes (connection\u003c\u003c conn bytes) else (return))\n```\n\nCheck out `EXAMPLE-CLIENT` and `EXAMPLE-SERVER` in `HTTP2-EXAMPLE` for\nbasic examples that perform complete HTTP request/responses. These\nfunctions use `CL+SSL` for secure connections, or `USOCKET` /\n`SB-BSD-SOCKETS` for plain connections.\n\nConnection lifecycle management\n-------------------------------\n\nDepending on the role of the endpoint you must initialize either a\n`CLIENT` or a `SERVER` object. Doing so picks the appropriate header\ncompression / decompression algorithms and stream management\nlogic. From there, you can subscribe to connection level events, or\ninvoke appropriate APIs to allocate new streams and manage the\nlifecycle. For example:\n\n```lisp\n;;; server\n\n(in-package :http2-example)\n\n(defvar server (make-instance 'server))\n\n(on server :stream (lambda (stream) ...))  ; process inbound stream\n(on server :frame (lambda (bytes) ...))  ; encoded HTTP/2 frames\n\n(ping server (lambda (payload) ...))  ; send ping, process pong\n\n(goaway server)  ; send goaway frame to the client\n\n;;; client\n\n(in-package :http2-example)\n\n(defvar client (make-instance 'client))\n\n(on client :promise (lambda (stream) ...))  ; process push promise\n\n(defparameter stream (new-stream client))  ; allocate new stream\n(headers stream '((\":method\" . \"post\")) :end-stream: nil)\n(data stream (buffer-simple \"Hello\") :end-stream t)\n```\n\nEvents emitted by the `CONNECTION` object:\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:promise\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003eclient role only, fires once for each new push promise\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:stream\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003eserver role only, fires once for each new client stream\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:frame\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires once for every encoded HTTP/2 frame that needs to be sent to the peer\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nStream lifecycle management\n---------------------------\n\nA single HTTP/2 connection can\n[multiplex multiple streams](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#REQUEST_RESPONSE_MULTIPLEXING)\nin parallel: multiple requests and responses can be in flight\nsimultaneously and stream data can be interleaved and\nprioritized. Further, the specification provides a well-defined\nlifecycle for each stream (see below).\n\nThe good news is, all of the stream management, and state transitions,\nand error checking is handled by the library. All you have to do is\nsubscribe to appropriate events (marked with \":\" prefix in diagram\nbelow) and provide your application logic to handle request and\nresponse processing.\n\n```\n                         +--------+\n                    PP   |        |   PP\n                ,--------|  idle  |--------.\n               /         |        |         \\\n              v          +--------+          v\n       +----------+          |           +----------+\n       |          |          | H         |          |\n   ,---|:reserved |          |           |:reserved |---.\n   |   | (local)  |          v           | (remote) |   |\n   |   +----------+      +--------+      +----------+   |\n   |      | :active      |        |      :active |      |\n   |      |      ,-------|:active |-------.      |      |\n   |      | H   /   ES   |        |   ES   \\   H |      |\n   |      v    v         +--------+         v    v      |\n   |   +-----------+          |          +-_---------+  |\n   |   |:half_close|          |          |:half_close|  |\n   |   |  (remote) |          |          |  (local)  |  |\n   |   +-----------+          |          +-----------+  |\n   |        |                 v                |        |\n   |        |    ES/R    +--------+    ES/R    |        |\n   |        `-----------\u003e|        |\u003c-----------'        |\n   | R                   | :close |                   R |\n   `--------------------\u003e|        |\u003c--------------------'\n                         +--------+\n```\n\nFor sake of example, let's take a look at a simple server implementation:\n\n```lisp\n(defvar conn (make-instance 'server))\n\n; emits new streams opened by the client\n(on conn :stream\n  (lambda (stream)\n    (on stream :active\n      (lambda () ))  ; fires when stream transitions to open\n    (on stream :close\n      (lambda (err) ))  ; stream is closed by client and server\n\n    (on stream :headers\n      (lambda (head) ))  ; header callback\n    (on stream :data\n      (lambda (chunk) ))  ; body payload callback\n\t  \n    (on stream :half-close\n\t  (lambda ()\n\t    ; ... generate response ...\n\t\t; send response\n\t\t(headers stream '((\":status\" . \"200\")\n\t\t                  (\"content-type\" . \"text/plain\")))\n\t\t\t\t\t\t  \n        ; split response between multiple DATA frames\n\t\t(let ((chunk1 (buffer-simple \"stuff \"))\n\t\t      (chunk2 (buffer-simple \"more stuff\")))\n\t\t  (data stream chunk1 :end-stream nil)\n\t\t  (data stream chunk2))))))\n```\n\nEvents emitted by the `STREAM` object:\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:reserved\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires exactly once when a push stream is initialized\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:active\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires exactly once when the stream become active and is counted towards the open stream limit\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:headers\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires once for each received header block (multi-frame blocks are reassembled before emitting this event)\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:data\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires once for every DATA frame (no buffering)\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:half_close\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires exactly once when the opposing peer closes its end of connection (e.g. client indicating that request is finished, or server indicating that response is finished)\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:close\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires exactly once when both peers close the stream, or if the stream is reset\u003c/td\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cb\u003e:priority\u003c/b\u003e\u003c/td\u003e\n    \u003ctd\u003efires once for each received priority update (server only)\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\nFlow control\n-----------\n\nMultiplexing multiple streams over the same TCP connection introduces\ncontention for shared bandwidth resources. Stream priorities can help\ndetermine the relative order of delivery, but priorities alone are\ninsufficient to control how the resource allocation is performed\nbetween multiple streams. To address this, HTTP/2 provides a simple\nmechanism for\n[stream and connection flow control](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#_flow_control).\n\nConnection and stream flow control is handled by the library: all\nstreams are initialized with the default window size (64KB), and\nsend/receive window updates are automatically processed - i.e. window\nis decremented on outgoing data transfers, and incremented on receipt\nof window frames. Similarly, if the window is exceeded, then data\nframes are automatically buffered until window is updated.\n\nThe only thing left is for your application to specify the logic as to\nwhen to emit window updates:\n\n```lisp\n(buffered-amount conn)      ; check amount of buffered data\n(conn-window conn)          ; check current window size\n(window-update conn 1024)   ; increment connection window by 1024 bytes\n\n(buffered-amount stream)    ; check amount of buffered data\n(stream-window stream)      ; check current window size\n(window-update stream 2048) ; increment stream window by 2048 bytes\n```\n\nServer push\n-----------\n\nAn HTTP/2 server can\n[send multiple replies](http://chimera.labs.oreilly.com/books/1230000000545/ch12.html#HTTP2_PUSH)\nto a single client request. To do so, first it emits a \"push promise\"\nframe which contains the headers of the promised resource, followed by\nthe response to the original request, as well as promised resource\npayloads (which may be interleaved). A simple example is in order:\n\n```lisp\n(defvar conn (make-instance 'server))\n\n(on conn :stream\n  (lambda (stream)\n    (on stream :headers\n\t  (lambda (head) ... ))\n    (on stream :data\n\t  (lambda (chunk) ... ))\n\n    ; fires when client terminates its request (i.e. request finished)\n    (on stream :half-close\n\t  (lambda ()\n\t    (let ((head '((\":status\" . \"200\")\n\t\t              (\":path\" . \"/other_resource\")\n\t\t\t\t\t  (\"content-type\" . \"text/plain\")))\n              promise)\n          (promise stream head\n\t\t    (lambda (push)\n\t\t\t  (headers push ...)\n\t\t\t  (setf promise push))))\n\t\t\t  \n        (headers stream '((\":status\" . \"200\")\n\t\t                  (\"content-type\" . \"text/plain\")))\n        (data stream response-chunk :end-stream nil)\n\t\t(data promise payload)\n\t\t(data stream last-chunk))))))\n```\n\nWhen a new push promise stream is sent by the server, the client is\nnotified via the `:promise` event:\n\n```lisp\n(defvar conn (make-instance 'client))\n(on conn :promise\n  (lambda (push)\n    ; process push stream\n\t))\n```\n\nThe client can cancel any given push stream (via `STREAM-CLOSE`), or\ndisable server push entirely by sending the appropriate settings frame\n(note that below setting only impacts server \u003e client direction):\n\n```lisp\n(settings client :streams 0)  ; setting max limit to 0 disables server push\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakamai%2Fcl-http2-protocol","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakamai%2Fcl-http2-protocol","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakamai%2Fcl-http2-protocol/lists"}