<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>APIs and automation on errno</title><link>https://errno.jofrey.eu/categories/apis-and-automation/</link><description>Recent content in APIs and automation on errno</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 14 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://errno.jofrey.eu/categories/apis-and-automation/index.xml" rel="self" type="application/rss+xml"/><item><title>curl exits 3 on a Jenkins API call: square brackets in tree= are not URL-safe</title><link>https://errno.jofrey.eu/posts/jenkins-api-tree-parameter-brackets-break-curl/</link><pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/jenkins-api-tree-parameter-brackets-break-curl/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;I was automating a build trigger against a Jenkins controller from a shell script: start a parameterised&#10;job, then follow the queue item until it turns into a real build. The queries worked perfectly when I&#10;pasted them into a browser with my session cookie. From the shell, the same URL produced nothing at all:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -u ci-user:$JENKINS_TOKEN &amp;#34;$JENKINS/queue/item/42/api/json?tree=executable[number]&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl: (3) bad range in URL position 45:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;https://ci.example.com/queue/item/42/api/json?tree=executable[number]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ^&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ echo $?&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every nested &lt;code&gt;tree=&lt;/code&gt; expression behaved the same way. Listing a job&amp;rsquo;s parameter definitions:&lt;/p&gt;</description></item><item><title>MCP server dies at startup after the mcp package resolves to 2.0.0</title><link>https://errno.jofrey.eu/posts/mcp-server-crashes-at-startup-after-mcp-2-0-0/</link><pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/mcp-server-crashes-at-startup-after-mcp-2-0-0/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;Two stdio MCP servers that had worked for weeks stopped appearing in the client. No tools, no error dialog&#10;— the server process simply exits during startup, and the client shows it as failed to connect. Running it&#10;by hand shows why:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Server&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;list_tools&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing in the project changed. That is the tell.&lt;/p&gt;&#10;&lt;h2 id="cause-an-unpinned-sdk-and-a-fresh-resolution"&gt;Cause: an unpinned SDK and a fresh resolution&lt;/h2&gt;&#10;&lt;p&gt;The server&amp;rsquo;s dependency spec was permissive:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mcp&amp;gt;=0.9.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; re-resolves on each run when there is no lock file in play, and the newest release satisfying that&#10;range became &lt;strong&gt;2.0.0&lt;/strong&gt;, which removed the decorator-style &lt;code&gt;Server.list_tools()&lt;/code&gt; API the server was written&#10;against. The code is fine; it is simply talking to a different major version than the one it was built for.&lt;/p&gt;</description></item><item><title>gpsoauth returns BadAuthentication on a 2FA Google account (app passwords do not help)</title><link>https://errno.jofrey.eu/posts/gpsoauth-badauthentication-2fa-master-token/</link><pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/gpsoauth-badauthentication-2fa-master-token/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;Getting a master token for a library that speaks Google&amp;rsquo;s mobile auth protocol (in my case &lt;code&gt;gkeepapi&lt;/code&gt; for&#10;Google Keep) starts with &lt;code&gt;gpsoauth&lt;/code&gt;. On an account with 2FA enabled it fails:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;gpsoauth&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;gpsoauth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perform_master_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;me@gmail.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;…&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;android_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Error&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;BadAuthentication&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ErrorDetail&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The reflex is to generate an &lt;strong&gt;app password&lt;/strong&gt; and retry. It fails the same way. App passwords work for&#10;IMAP/SMTP, not for this endpoint, so no amount of retrying the password path will produce a token.&lt;/p&gt;</description></item><item><title>Graylog 7 API: POST /api/streams returns 400 "entity cannot be null"</title><link>https://errno.jofrey.eu/posts/graylog-7-create-stream-api-400-entity-cannot-be-null/</link><pubDate>Wed, 26 Aug 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/graylog-7-create-stream-api-400-entity-cannot-be-null/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;A stream-creation call that worked against Graylog 6 now fails:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS -u &amp;#34;$USER:$PASS&amp;#34; -H &amp;#39;X-Requested-By: cli&amp;#39; -H &amp;#39;Content-Type: application/json&amp;#39; \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &amp;#39;{&amp;#34;title&amp;#34;:&amp;#34;nginx&amp;#34;,&amp;#34;description&amp;#34;:&amp;#34;nginx access logs&amp;#34;,&amp;#34;remove_matches_from_default_stream&amp;#34;:false}&amp;#39; \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://graylog.example.com/api/streams&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/1.1 400 Bad Request&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{&amp;#34;type&amp;#34;:&amp;#34;ApiError&amp;#34;,&amp;#34;message&amp;#34;:&amp;#34;entity cannot be null&amp;#34;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing in the message points at the payload shape, so the natural assumption — a missing required field&#10;such as &lt;code&gt;index_set_id&lt;/code&gt; — sends you down the wrong path.&lt;/p&gt;&#10;&lt;h2 id="cause"&gt;Cause&lt;/h2&gt;&#10;&lt;p&gt;In Graylog 7 the endpoint takes a &lt;code&gt;CreateEntityRequest&lt;/code&gt;: the stream itself moved one level down, under an&#10;&lt;code&gt;entity&lt;/code&gt; key. Send the same fields wrapped:&lt;/p&gt;</description></item><item><title>MusicBrainz ws/2 returns 503 "currently busy", and the track lengths you need are often missing</title><link>https://errno.jofrey.eu/posts/musicbrainz-ws2-503-user-agent-and-missing-durations/</link><pubDate>Sat, 22 Aug 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/musicbrainz-ws2-503-user-agent-and-missing-durations/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;I was matching a local album against an online release to recover a correct tracklist — order, titles and&#10;durations — so a media library could be tagged from something better than filenames. The first half of that&#10;job is a lookup against the MusicBrainz web service, and it kept dying:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/1.1 503 Service Unavailable&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The body is a short page saying the service is currently busy and to try again later. Nothing else: no&#10;&lt;code&gt;429&lt;/code&gt;, no &lt;code&gt;Retry-After&lt;/code&gt; header worth trusting, no hint about which part of the query was expensive. The&#10;failure is not correlated with how long the script has been running either — it hits the very first request&#10;of a batch as happily as the tenth.&lt;/p&gt;</description></item><item><title>Teams incoming webhook returns HTTP 200 but no message appears</title><link>https://errno.jofrey.eu/posts/teams-incoming-webhook-returns-200-but-no-message/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/teams-incoming-webhook-returns-200-but-no-message/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;A monitoring or automation job posts to a Teams incoming webhook. The client is happy:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -sS -i -H &amp;#39;Content-Type: application/json&amp;#39; \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &amp;#39;{&amp;#34;text&amp;#34;:&amp;#34;certificate renewed&amp;#34;}&amp;#39; \&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;https://example.webhook.office.com/webhookb2/…&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;HTTP/1.1 200 OK&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;acme.sh&lt;/code&gt; reports &lt;code&gt;send success&lt;/code&gt;. Nothing arrives in the channel. No error, no bounce, no entry anywhere.&lt;/p&gt;&#10;&lt;h2 id="the-proof-is-in-the-response-headers"&gt;The proof is in the response headers&lt;/h2&gt;&#10;&lt;p&gt;Look past the status line:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-nanoproxy: 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-proxyerrorhresult: 0x80070036&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x-proxyerrormessage: The network is busy.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That is not a transient condition despite what it says. Microsoft retired Office 365 connectors, and the&#10;endpoints now answer &lt;code&gt;200&lt;/code&gt; at a proxy while discarding the payload. Any client that only checks the status&#10;code will report success forever, which is why this failure survives in cron jobs and alerting scripts for&#10;months.&lt;/p&gt;</description></item><item><title>Reading and writing MediaWiki pages from a script: action=raw and the two-token login</title><link>https://errno.jofrey.eu/posts/mediawiki-api-write-page-reader-mode-redirect/</link><pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/mediawiki-api-write-page-reader-mode-redirect/</guid><description>&lt;h2 id="reading-skip-the-html-ask-for-raw-wikitext"&gt;Reading: skip the HTML, ask for raw wikitext&lt;/h2&gt;&#10;&lt;p&gt;Fetching an article URL with a normal client and parsing the HTML is where most scripts start, and it goes&#10;wrong in a way that is easy to miss: a plain &lt;code&gt;GET&lt;/code&gt; of the page URL can land you on &lt;strong&gt;Recent Changes&lt;/strong&gt;&#10;instead of the article — a redirect that a browser resolves invisibly but that leaves your parser looking&#10;at completely different content and &amp;ldquo;working&amp;rdquo;.&lt;/p&gt;</description></item><item><title>Redmine REST: POST /uploads.json returns 404, and the API key only works in the query string</title><link>https://errno.jofrey.eu/posts/redmine-rest-uploads-404-and-api-key-auth/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/redmine-rest-uploads-404-and-api-key-auth/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;Attaching a file to a Redmine issue over REST is documented as a two-step dance. First you push the raw&#10;bytes to &lt;code&gt;/uploads.json&lt;/code&gt;, which answers with a &lt;code&gt;token&lt;/code&gt;. Then you create or update the issue and reference&#10;that token in an &lt;code&gt;uploads&lt;/code&gt; array. Step two is the interesting one, so that is where I expected trouble.&#10;Step one never got that far:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -X POST &lt;span class="s2"&gt;&amp;#34;https://redmine.example.com/uploads.json?key=&amp;lt;API_KEY&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s1"&gt;&amp;#39;Content-Type: application/octet-stream&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --data-binary @report.pdf&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;404&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not &lt;code&gt;401&lt;/code&gt;, not &lt;code&gt;403&lt;/code&gt;, not &lt;code&gt;422&lt;/code&gt; — &lt;code&gt;404&lt;/code&gt;. And since every attachment upload in the REST API starts by&#10;obtaining a token from exactly this endpoint, that single status code means there is no way to attach a file&#10;to an issue through the API on this instance. Not a harder way, not a slower way: none.&lt;/p&gt;</description></item><item><title>Gmail throws away your &lt;style&gt; block: generating HTML mail that survives the client</title><link>https://errno.jofrey.eu/posts/gmail-strips-the-style-block-in-html-mail/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/gmail-strips-the-style-block-in-html-mail/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;The job was small: assemble a short report from data I already had, put the numbers in a table, and leave&#10;the result in Drafts so a human could read it before it went anywhere. The HTML part looked right when I&#10;opened the generated file in a browser — bordered table, sane padding, header row picked out. Then I&#10;created the draft through the Gmail API, opened it in the Gmail web UI, and got something else entirely:&lt;/p&gt;</description></item><item><title>When an MCP wrapper hides API fields: set Redmine parent, due date and estimate over REST</title><link>https://errno.jofrey.eu/posts/redmine-mcp-wrapper-missing-fields-use-rest/</link><pubDate>Wed, 17 Jun 2026 00:00:00 +0000</pubDate><guid>https://errno.jofrey.eu/posts/redmine-mcp-wrapper-missing-fields-use-rest/</guid><description>&lt;h2 id="symptom"&gt;Symptom&lt;/h2&gt;&#10;&lt;p&gt;An agent-facing MCP wrapper for Redmine exposes issue creation and updates, but only a subset of the&#10;fields:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;subject, description, assigned_to_id, status_id, priority_id, done_ratio&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So a task can be created — but it cannot be attached to a parent, given a deadline, or given an estimate.&#10;There is no error to debug; the parameters simply do not exist in the tool schema, which is a worse failure&#10;mode, because the model happily reports success on a partially created issue.&lt;/p&gt;</description></item></channel></rss>