fix(meta): guard non-string mimetype before image check (fixes #2678) - #2691
fix(meta): guard non-string mimetype before image check (fixes #2678)#2691MatheusMartinho wants to merge 2 commits into
Conversation
…ion-foundation#2678) mimeTypes.lookup() returns `false`, not `undefined`, when it cannot resolve a type, e.g. a Chatwoot ActiveStorage audio URL with no file extension. Optional chaining only short-circuits on null/undefined, so `false?.startsWith('image/')` threw "TypeError: mimetype?.startsWith is not a function" in sendMessageWithTyping() before the request reached Meta. Check `typeof === 'string'` before calling startsWith; behavior for string mimetypes is unchanged.
Reviewer's guide (collapsed on small PRs)Reviewer's GuideThis PR hardens the WhatsApp Business integration by ensuring mime type checks only run on string values, preventing crashes when mime-type resolution returns Sequence diagram for guarded mimetype image check in WhatsApp Business servicesequenceDiagram
actor Chatwoot
participant BusinessStartupService
participant mimeTypes
Chatwoot->>BusinessStartupService: sendAttachment(url)
BusinessStartupService->>mimeTypes: lookup(url)
mimeTypes-->>BusinessStartupService: mimetype=false
BusinessStartupService->>BusinessStartupService: processAudio sets message[mimetype]=false
alt typeof message[mimetype] === 'string'
BusinessStartupService->>BusinessStartupService: isImage = message[mimetype].startsWith('image/')
else typeof message[mimetype] !== 'string'
BusinessStartupService->>BusinessStartupService: isImage = false (no crash)
end
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- Since
mimetypeis known to bestring | false, consider normalizing it at the point of assignment (e.g., mappingfalseto'') or encapsulating the image check in a small helper to avoid scatteredtypeof === 'string'guards and keep usage sites simpler. - The new inline comment is quite detailed and Chatwoot-specific; you might trim it to a shorter, implementation-focused note about
mimeTypes.lookupreturningfalseso future readers understand the guard without cross-referencing external flows.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Since `mimetype` is known to be `string | false`, consider normalizing it at the point of assignment (e.g., mapping `false` to `''`) or encapsulating the image check in a small helper to avoid scattered `typeof === 'string'` guards and keep usage sites simpler.
- The new inline comment is quite detailed and Chatwoot-specific; you might trim it to a shorter, implementation-focused note about `mimeTypes.lookup` returning `false` so future readers understand the guard without cross-referencing external flows.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Thanks for the review. On the comment: agreed, trimmed it to the mechanism and dropped the Chatwoot-specific context, since this file is the Meta channel service. On normalizing at assignment: I looked at whether the guards would be scattered, and I did consider normalizing at the producer. The reason I did not is that That said, this is a design call for the maintainers, not for me. If you would rather have the producers hand back a clean value, say the word and I will move the normalization into |
📋 Description
mimeTypes.lookup()returnsfalse, notundefined, when it cannot resolve a type. Chatwoot serves attachments through Rails ActiveStorage URLs that carry no file extension, so for every Chatwoot audio the lookup inprocessAudio()returnsfalseandprepareMedia.mimetypeis set tofalse.Line 1128 of
whatsapp.business.service.tsthen runs:Optional chaining only short-circuits on
nullandundefined.falseis neither, so the call reachesfalse.startsWithand throwsTypeError: mimetype?.startsWith is not a functionbefore the request ever leaves for Meta.This changes the check to an explicit type guard:
🔗 Related Issue
Closes #2678
🧪 Type of Change
🧪 Testing
I reproduced the failure without a Chatwoot instance, by running the repository's own
mime-typesdependency against the exact expression from line 1128:With the guard in place all four cases behave, and the existing behaviour for real MIME strings is unchanged:
mimetypeisImagefalse(Chatwoot)falseundefinedfalse"audio/ogg"false"image/png"trueGates run locally on this branch (based on the current
developtip):npm run lint:check— cleannpm run build(tsc --noEmit+tsup) — passesI did not run
npm test: the script points at./test/all.test.ts, which does not exist ondevelop.📸 Screenshots (if applicable)
N/A — backend-only change.
✅ Checklist
📝 Additional Notes
Why only audio is affected. In
chatwoot.service.ts,sendAttachment()downloads the file specifically to read itsContent-Typewhen the extension-based lookup fails:It then uses that value to route the message, but on the audio branch it passes only the raw URL to
audioWhatsapp()and the resolved MIME type is dropped.processAudio()re-derives it from the same extension-less URL and getsfalse.This looks like an oversight rather than intent. The line above, in
chatwoot.service.ts, already normalizes the same call with|| ''. Thefalsereturn value is understood elsewhere in the codebase, it is just not handled at this consumer.processAudio()andprocessMedia()even declarelet mimetype: string | false, so the type is documented in the signature.Why the guard is here and not in the producers.
startsWith('image/')has exactly one call site insrc/, this one.processMedia()(lines 1379 and 1383 ondevelop) uses the samestring | falsepattern and feeds the same line, so an image or document sent from an extension-less URL would hit the identical crash. One guard at the point of use covers every producer. NormalizingfalsetoundefinedinprocessAudio()alone would fix audio only.No behaviour change on the audio path. For audio,
isImageis only read inside a spread that already excludesaudio, so the payload sent to Meta is byte-for-byte the same. The change removes the exception and nothing else.Happy to move the normalization into
processAudio()andprocessMedia()instead if you would prefer the producers to hand back a clean value, and equally happy to be told I have misread something. I am new to this codebase.