Fixing Mastodon URL truncation in n8n
I have been working on automating my POSSE setup, and ended up trying n8n and it has been a great experience, there are so many plugins that it’s mostly just wiring things together.
But, I discovered that despite being within the character limit, one of my URLs were truncated when posting to Mastodon. This is because Mastodon does something non-obvious: it treats all URLs as 23 characters, no matter if they’re shorter or longer.
This is because they copied this behavior from Twitter, which ran all links through their t.co link shortener. The reason is that Mastodon doesn’t want you to use a link shortener, since it hides the real URL (i.e. makes rick rolling easier 😜).
So the module truncated the text in the status. Since it wasn’t URL-aware, all I got was a broken link.
I decided to fix it myself, because dammit, I’m a programmer. It’s an unfamiliar codebase I know nothing about, so I rolled up my sleeves and asked Claude, “Pretty please, can you fix the problem and add tests?” (ah the modern world we live in)
The key constraint: I needed to understand every line of code before shipping it, and it had to pass my quality standards because my name would be on the PR. Happily, that worked out becuse my PR was accepted. 🙂
Here’s how I worked through it over three days. All commits are here if you want to step through them. I really intend to share these devlogs earlier in the future, but I got lazy 😅
The rough notes
Lightly edited for the web, and I’ve kept the day breaks in.
2025-10-08
I discovered that my mastodon poster truncated the URL (which seems to indicate that the n8n plugin does a naïve counting of characters, whereas it should only count 23 chars/URL, because Mastodon don’t penalize longer URLs so we don’t need link shorteners.
So I looked at the source for the package in question and found the 500 limit (I had looked at this before) and knew it was hardcoded, and then I decided to download the repo and ask Claude to find the bug and suggest a fix:
It then came up with a plan that I rejected because I wondered if there any existing packages that implement this logic, since the Javascript community is quite keen on tiny packages
The rejected plan to implement
So I asked Claude to look for a package:
It searched and didn’t find anything (it was a bit slow, so I decided to look as well, and didn’t find anything obvious), when I looked back after giving up it asked to download the original Ruby source for the status length validation and then came up with new search patterns
It then found twitter-text and suggested to import it and use
Plan to use twitter-text
I wasn’t sure about all this because looking at the repo it looks like a big dependency, and maybe it has too much extra stuff that doesn’t really come into play for Mastodon?
It then went off and did a bunch of searches around east-asian specific character encodings (Japanese and Chinese alphabets as far as I can tell)
I then got a new plan that looks better, but I still wonder about the when possible for preserving URLs (I expect it is that if there’s not 23 chars left, then it has to cut, so I will have it clarified in a more detailed plan)
The new plan that includes when possible for preserving URLs
I asked what when possible means and guessed some cases (I forgot to save the prompt)
The details for when it's not possible to save the URL
So, now to handle this I will ask it to write this high-level plan into high-level.plan.md and then to create a detailed plan that could be implemented by a junior engineer, so we then can ask sub agents to implement them step-by-step. I saw the plan and it suggested to drop URLs that went over the limit, so I asked it to instead keep them.
I haven’t added any general/background context to this repo about my preferences, so I wonder if this will be enough to give me something useful in this repo:
It came up with a detailed plan, it looked good enough (skimmed it), so I asked claude to:
Then it couldn’t run any commands, likely because I never ran npm install in the folder so I did that and verified that npm test worked and then asked Claude to try again.
Next up, execute the plan: Now plan to execute @plan.md with subagents and marking off which steps has been done in the plan as they finish..
The plan
I then set Claude to execute and left some questions for myself to answer after we were done:
- Are there more places we should use this new validation/truncation logic?
- Is our new code implemented similarly to the rest of the code in this repository?
- What questions should I ask you to verify that we don’t break something?
00:34 It’s late, so time to let Claude spin away while I prepare for sleep.
00:50 Okay, while prepping for sleep it finished and I had a look on the new tests the tests have some problems that we’ll need to iterate on:
- Doesn’t assert the exact values we’re looking for
- Doesn’t make the 500 or 23 constants and are instead magic numbers, let’s name them
- The truncation logic is too smart and it should just cut the URL and leave an ugly “scar” if that’s what’s required, that’s fine. We can iterate on that
I think the way to work with this is to create a fork and do my changes in a repo with small focused commits, and then show the work as I commit, so I indicate how I grew the solution
Now, sleep for reals
2025-10-10
Okay, back at it again after a day’s break, got some tunes on and now to review.
Okay, to help me document how I’m working on this I will commit the change I got from Claude, and then I’ll do my changes from there and commit as I do them. Doing this so that it’ll be easier to blog about it, and I don’t know if that workflow will be what I want/need, so we’ll see.
Reviewing
Starting the review by looking at the status.test.ts since I’m supposed to have good tests that tell me what’s going on, so that’ll be the lense through which I review this.
First off, I got header comments explaining the change that I don’t need, so nixing ’em, and I think all these tests belong in a describe that shows that the tests belong together. Commit
Then, let’s get going with the first test “should preserve long URL when using URL-aware character counting” doesn’t do one that will hit the problem, and the test doesn’t check the count is what we expect it to be (which is also a problem). okay, changed the test to only contain a URL and then verify that the expected status is exactly 23 characters long, which passes. Commit
Next is a test with multiple long URLs, which is useful, make sure we don’t do this for only one URL in the status. The URLs are not pushing it as far as it should, so I made both URLs more than the total length of a status (80 -> 500)
The text is a bit annoying so simplifying it for my own sake to make the length calculation simpler, it’s a list of links one per line (2*2+1 = 5 + 23 + 23 = 51):
And about now I’m annoyed at the way the mock implementation has so much setup, it doesn’t follow my coding style (too much repetition), but this isn’t my project, and it seems to be how all other tests are written in this file so we keep it. Consistency and avoiding surprises is more important than my personal style.
Now comes the really nasty bit: Claude decided to truncate text instead of URLs which isn’t what I want, if the URL is placed so that it’s unavoidable that it will be cut, then cut it. Don’t remove it, and don’t remove text before it. It’s better to indicate that something went wrong (by showing a cut URL) than silently dropping it (I’d rather go boom than be silent, been saying this for a very looong time, mostly living that philosophy :p)
So I think this particular test will have to change. And about now I’m regretting that I didn’t commit already because I feel I should’ve done it per test instead. Because now I’m about to make the test red, so I want to make sure I can change it cleanly. So, I’ll undo my changes (thanks IntelliJ for having infinite undo in the same session), commit, and then redo per function. That took ~5min to fix, and while not really necessary, I feel a lot better from going back and redoing and creating clean commits to show the work (so the commits linked was made before this).
Okay, now, this test I’ll also change up a bit:
- Loong text,
ax480, a space, and a url which means the effective length will be 505 so the URL will be cut - I changed the assertion on the
sentStatusto be the naively cut status instead (since the URL can’t ever fit now, so it will just have to mangle the URL) - And as expected, the test is now failing, so that’s good. Now, let’s ask Claude to fix the failing test I’ve created
- I highlighted the test in IntelliJ and opened up Claude and said (mostly because I haven’t looked at the code, this might actually be faster to do manually, so I’ll look at it as the fresh Claude session is figuring out what’s going on):
Found the place where this is, it’s in nodes/Mastodon/Mastodon_Methods.ts and the new truncateWithUrlPreservation.
And I see that Claude is spinning and reading what it’s doing I realize I made a logical error: if Mastodon always counts URLs as 23 chars, I can’t truncate a URL and make it work. The logic Claude made was reasonable since I told it to never raise an exception. So that means I need to make this case an explicit negative case, that it will fail the validation, and I could exemplify that better by making it a really short but valid URL (ex https://example.com which is 19 chars)
Okay, asked Claude to get to it, but it’s struggling with the logic and I’m realizing that I should just add another test alongside this where we have a long URL at the start and a really long text at the end, that we can just truncate as needed, so it can see the logic better.
BUT, for right now, I’m gonna start a stew so I’ll go do that and then let it… stew for a coupe of hours so I can continue then
20:15 back and doing stuff again
I added another test that swaps the order so it’s <url> <text> and now it’s failing too, so let’s clear the session for Claude and start over
Claude came back with a plan to let the server fail instead of us truncating long tests, which won’t make the tests pass, so I prompted:
Claude then suggested a flow that seems to work:
And Claude implemented the fix and did it TDD style and highlighted that we have more code than necessary to pass, so I need to write another test case that exercises all the extra code we have in truncateWithUrlPreservation
2025-10-11
14:43 The next day, I ended up watching TV yesterday, and today I’m feeling sluggish
But I was reflecting about this yesterday and I’m a bit unsure about how to write a test that refuses the current “fix” of " if the actual is less than 500 let through" because it’s true since the edge-cases it won’t do it for is basically where the URL is wrong, BUT, the URL could be wrong at the start and the text after is okay to be truncated. I.e. cases where say https://example.com (19) x 21 = = 399 actual but the effective length is 483 and if I add 18 characters of text it’ll fail and could be truncated.
Maybe that’s the case to do, because it’ll force it to handle it correctly.
I ended up changing the “this can be truncated” test so the URL is shorter and the text is exactly so it should pass, and then pointed out that we need to hint that we need the special logic if there are URLs in the text.
Claude came back with a plan:
And that update gave me the logic I wanted, so now let’s see what the change looks like and it’s good.
As I now look at it and am preparing the commit message I realize, this doesn’t fit the original design of this library/n8n node: it was blindly truncating, so I could also just truncate just before this URL so we can post. It doesn’t fit with how I discovered this problem, which was a truncated URL (which was still longer than 23 characters but wrong).
So, with that, I will instead say: nix the URL because it goes with the grain of the library and people like me have to ensure they truncate correctly. It’s the least surprising thing we can do. But, I have done all this work and despite realizing that I went down the wrong path, I should commit what I got so I can see the changes. I learned something and that’s still a win.
Okay, done , and I asked Claude to look at the comments I’ve tried to add to clarify the logic and it suggested some improvements to them which I’ve folded into the commit.
I’m not sure if using Claude here has saved me time, but I have been doing it in low energy and while watching TV, so upside is that it got done compared to that I likely wouldn’t have put in the full energy otherwise.
Although, partly that’s because when fixing the bad stuff I didn’t want to go in and learn how all the code worked, becasue I’m relying on the tests to tell me what’s working and not, so that’s pure laziness on my part. And the fact it dug through the full codebase and made a first pass when I was turning in for the night is sooooo worth it. I’m sure I would’ve spent more time figuring out the codebase on my own otherwise.
But, I am also saying this before I have manually tested any of this. I have only run the tests so far and not plugged it into n8n and seen if it works. So my optimism might still be premature.
Okay, updated one test file and the corresponding business logic, let’s start with the next test file (which I changed a little for the previous task, so I know there’s work to do).
2025-10-12
I did some final cleanup yesterday and didn’t document it all, it’s only available in the commits, but I basically went over the changes as I would any PR at work and then raised it. The maintainer looked at it during the day and then left a nice note thanking me for being thorough and documenting my code well 🥰
Hey Björn!
Thank you so much for your outstanding contribution with PR #4! 🚀
Your pull request to fix the URL character counting behavior was absolutely fantastic - it addressed a real problem that many users probably didn’t even realize they were experiencing. The fact that Mastodon counts all URLs as exactly 23 characters regardless of their actual length is such an important detail, and your implementation handles it perfectly.
What I especially appreciated about your work:
✨ Thorough Research - You referenced the actual Mastodon source code to validate the behavior
🧪 Comprehensive Testing - 19 new tests covering all the edge cases!
📚 Excellent Documentation - Clear comments explaining the “why” behind the logic
🔧 Smart Implementation - The decision to remove entire URLs rather than break them is brilliant
🔒 Clean Integration - Minimal changes to existing code while adding powerful functionalityThe Impact:
Your fix means users won’t have their URLs unexpectedly broken when posting longer content. That’s a real quality-of-life improvement that will prevent confusion and frustration.
Code Quality:
The implementation is clean, well-tested, and follows all the best practices. It’s the kind of contribution that makes maintaining open source projects a joy!
Would you be interested in contributing more to this project? Your expertise with Mastodon’s APIs and attention to detail would be incredibly valuable.
Thanks again for taking the time to identify this issue, implement a robust solution, and provide such thorough testing. This is exactly the kind of contribution that makes the open source community amazing! 🙌
Best regards
As they said, these interactions make open source fun because it’s people working and building stuff together 🙂