Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vague error message for 422 Unprocessable Content #885

Open
prplecake opened this issue Jan 29, 2025 · 9 comments · May be fixed by #887
Open

Vague error message for 422 Unprocessable Content #885

prplecake opened this issue Jan 29, 2025 · 9 comments · May be fixed by #887
Labels
Support Requests for Help regarding setup and usage

Comments

@prplecake
Copy link

I've been banging my head against the wall for the last few hours trying to figure this out:

Image

I think I narrowed it down to a missing Content-Length header, but it would be really, really nice if the error returned from the server was accurate. Not every tool out there automatically calculates that header. (Looking at you, Fetch API.)

@Kovah
Copy link
Owner

Kovah commented Jan 29, 2025

Someone had a similar issue here: #856 (comment)

Could you try to send the exact same request to the LinkAce demo? The API key is available in the user settings when logged in.

Also, which version do you use?

@prplecake
Copy link
Author

I see the same behavior on the demo. If Content-Length is missing, you get the "URL is required" error.

Missing Content-Length Request:
POST /api/v1/links HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer <sensitive>
Cache-Control: no-cache
Postman-Token: 98d8e94f-a68e-4fc8-badf-3974701b34b8
Host: demo.linkace.org
 
{
"url": "https://duckduckgo.com"
}

Response:

HTTP/1.1 422 Unprocessable Entity
Date: Wed, 29 Jan 2025 16:13:10 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
access-control-allow-origin: *
alt-svc: h3=":443"; ma=86400
Cache-Control: no-cache, private
x-ratelimit-limit: 60
x-ratelimit-remaining: 57
cf-cache-status: DYNAMIC
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=OLOp4%2BaGVh9aAYOAMjJl%2FShvgVUPoo9za9Fn7eoNAgJjRwDcKn%2FPfEjM8XHFMYLLF9ubtz9%2BGGRttkkICBjTZcp6wm8wXoL1kS2xusggwFtFQgqNSr%2FZp6MYujL4q%2BHzAOUS"}],"group":"cf-nel","max_age":604800}
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Strict-Transport-Security: max-age=15552000; includeSubDomains; preload
Server: cloudflare
CF-RAY: 909a82091ffbacff-MSP
server-timing: cfL4;desc="?proto=TCP&rtt=18105&min_rtt=12435&rtt_var=8135&sent=13&recv=11&lost=0&retrans=0&sent_bytes=2215&recv_bytes=1221&delivery_rate=350428&cwnd=241&unsent_bytes=0&cid=a63414b278280261&ts=6353&x=0"
 
{"message":"The url field is required.","errors":{"url":["The url field is required."]}}
Same exact request with Content-Length Request:
POST /api/v1/links HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer <sensitive>
Cache-Control: no-cache
Postman-Token: cf30576f-cde5-4174-a5c3-af7a93f0f125
Host: demo.linkace.org
Content-Length: 41
 
{
"url": "https://duckduckgo.com"
}

Response:

HTTP/1.1 200 OK
Date: Wed, 29 Jan 2025 16:13:04 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
access-control-allow-origin: *
alt-svc: h3=":443"; ma=86400
Cache-Control: no-cache, private
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-ratelimit-limit: 60
x-ratelimit-remaining: 58
x-xss-protection: 1; mode=block
cf-cache-status: DYNAMIC
Report-To: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=iNDSvzIytnxufV%2FXyS1jjrmBSbdQoXKrRe2hoIKPZonPmg%2BXe0kFPqXOBlSswlrNAiI3tdVU2GsxJ6whQ9h15vU00wY9BESbvuO64eCUYHK44dlrYWT4EJo%2FGPinn2U7HyKr"}],"group":"cf-nel","max_age":604800}
NEL: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
Strict-Transport-Security: max-age=15552000; includeSubDomains; preload
Server: cloudflare
CF-RAY: 909a81e339d9acff-MSP
server-timing: cfL4;desc="?proto=TCP&rtt=18906&min_rtt=14125&rtt_var=8712&sent=4&recv=4&lost=0&retrans=0&sent_bytes=203&recv_bytes=911&delivery_rate=103362&cwnd=238&unsent_bytes=0&cid=a63414b278280261&ts=426&x=0"
 
{"url":"https:\/\/duckduckgo.com","title":"DuckDuckGo - Your protection, our priority.","description":"The Internet privacy company that empowers you to seamlessly take control of your personal information online, without any tradeoffs.","user_id":1,"icon":"link","thumbnail":"https:\/\/duckduckgo.com\/assets\/logo_social-media.png","updated_at":"2025-01-29T16:13:04.000000Z","created_at":"2025-01-29T16:13:04.000000Z","id":89,"tags":[],"lists":[]}

I saw this issue on 1.15.6 on my production instance. Looks like the demo is currently using 1.15.5. And before I upgraded to 1.15.6 yesterday, I was using and experiencing the same issue on 1.12.1.

@Kovah
Copy link
Owner

Kovah commented Jan 29, 2025

Could you try that again with the V2 demo? https://demo-v2.linkace.org/dashboard
The api token should be the same.

By the way: which tool do you use to make this request?

@Kovah
Copy link
Owner

Kovah commented Jan 30, 2025

curl -vvv --request POST \
  --url http://my-local-linkace-2.app/api/v2/links \
  --header 'Authorization: Bearer 1|VQw1dO9s....' \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --data '{
  "url": "https://duckduckgo.com/updates"
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host my-local-linkace-2.app:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.100.125
*   Trying 192.168.100.125:80...
* Connected to my-local-linkace-2.app (192.168.100.125) port 80
> POST /api/v2/links HTTP/1.1
> Host: my-local-linkace-2.app
> User-Agent: curl/8.7.1
> Authorization: Bearer 1|VQw1dO9s....
> accept: application/json
> content-type: application/json
> Content-Length: 45
>
* upload completely sent off: 45 bytes
< HTTP/1.1 200 OK
< Date: Thu, 30 Jan 2025 12:38:06 GMT
< Server: Apache/2.4.58 (Ubuntu)
< Vary: Authorization
< Cache-Control: no-cache, private
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 58
< Access-Control-Allow-Origin: *
< Transfer-Encoding: chunked
< Content-Type: application/json
<
* Connection #0 to host my-local-linkace-2.app left intact
{"url":"https:\/\/duckduckgo.com\/updates","title":"What\u2019s New in DuckDuckGo","description":"We\u2019re constantly making improvements to our product. Every quarter, we post the best of our most recent updates here.","user_id":2,"icon":"link","thumbnail":"https:\/\/duckduckgo.com\/static-assets\/image\/pages\/updates\/meta.png","updated_at":"2025-01-30T12:38:07.000000Z","created_at":"2025-01-30T12:38:07.000000Z","id":394,"tags":[],"lists":[]}

however

curl -vvv --request POST \
  --url http://my-local-linkace-2.app/api/v2/links \
  --header 'Authorization: Bearer 1|VQw1dO9s....' \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --header "Content-Length:" \
  --data '{
  "url": "https://duckduckgogo.com"
}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host my-local-linkace-2.app:80 was resolved.
* IPv6: (none)
* IPv4: 192.168.100.125
*   Trying 192.168.100.125:80...
* Connected to my-local-linkace-2.app (192.168.100.125) port 80
> POST /api/v2/links HTTP/1.1
> Host: linkace-apache.pmx.local
> User-Agent: curl/8.7.1
> Authorization: Bearer 1|VQw1dO9s...
> accept: application/json
> content-type: application/json
>
* upload completely sent off: 39 bytes
< HTTP/1.1 422 Unprocessable Content
< Date: Thu, 30 Jan 2025 12:46:03 GMT
< Server: Apache/2.4.58 (Ubuntu)
< Vary: Authorization
< Cache-Control: no-cache, private
< X-RateLimit-Limit: 60
< X-RateLimit-Remaining: 58
< Access-Control-Allow-Origin: *
< Transfer-Encoding: chunked
< Content-Type: application/json
<
* Leftovers after chunking: 498 bytes
* Connection #0 to host linkace-apache.pmx.local left intact
{"message":"The url field is required.","errors":{"url":["The url field is required."]}}

I think the behaviour is not entirely false in this context, as the error message is only sent as an addition to the actual issue: 422 Unprocessable Content

The client doing the request should be able to correctly send those requests, and content-length is at least for HTTP1.1 a required header when sending data and the transfer is not chunked.

Maybe there is a specific setting for the web server in front of LinkAce to handle this scenario, but I would not like to fix this edge case inside the application.

@Kovah Kovah added the Support Requests for Help regarding setup and usage label Jan 30, 2025
@prplecake
Copy link
Author

I've been using Postman to test some of these requests. Behavior is identical on the v2 demo.

I guess I got myself a bit confused yesterday focusing too much on the error message returned from LinkAce. I suppose I was expecting the message to explain the 422, which it technically doesn't, but I suppose is kind of technically correct as LinkAce must be trying to process an empty request or something.

I've mentioned it before, but I'm working on a firefox extension to save pages to LinkAce. Found some time to work on it again lately, and I'm not noticing my requests weren't identical from the extension vs my testing with Postman.

So, most of my trouble is stemming from not being a full-time JavaScript/TypeScript developer. I didn't realize the Fetch API doesn't automatically set the correct Content-Type headers, so the extension was sending Content-Type: test/plain;charset=UTF-8, which LinkAce doesn't want, hence the 422. Actually, in this example I think the server should return a 415 Unsupported Media Type, as the 422 Unprocessable Content suggests the server did understand the content-type, which is not true here.

Spent about 7 hours working on the extension yesterday and by the end of it my brain was apparently fried and I wasn't noticing all these details.

I maintain the opinion that the server should explain why it was unprocessable, and give a more accurate response status in the case of an incorrect Content-Type. There's a reason standards exist.

Worth noting there's also 411 Length Required, which should be returned if the server refuses to accept the request without a defined Content-Length, but most tools automatically calculate this, so this is a moot point. I don't know how much of this validation is configured in LinkAce vs being defined in the Framework.

@Kovah
Copy link
Owner

Kovah commented Jan 30, 2025

I will have a look if such cases can easily handled properly.

@prplecake
Copy link
Author

I hope I'm not coming off unthankful or anything. I really do enjoy using LinkAce, I decided to stop using Pinboard and this is a fantastic replacement.

Developing for the API has been a bit painful I have to admit.

I whipped this up to see if it would help, and it kind of would, but this also feels completely incorrect now that I know those other HTTP statuses exist. Plus this would have to be duplicated on all endpoints, and that feels wrong, too!

prplecake@dfe0639

@Kovah
Copy link
Owner

Kovah commented Jan 30, 2025

Hey, no worries. I am thankful for your input. 🙏 I have not yet worked with the LinkAce API beside doing some work via Zapier and with iOS shortcuts.
The commit looks interesting, I will check this and add it if everything works as expected.

@prplecake
Copy link
Author

I wonder if a Middleware would be a better place for it, and then it can apply to whatever routes it's configured for.

I wonder if something like this would be a better route to go:

class ValidateContentTypeHeader
{
    public function handle($request, Closure $next)
    {
        if ($request->header("Content-Type") === "application/json") {
            return $next($request);
        }

        return Response::json("Content-Type should be 'application/json'.", 415);
    }
}

I might keep playing around with this, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Support Requests for Help regarding setup and usage
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants