Skip to content

fix(meta): guard non-string mimetype before image check (fixes #2678) - #2691

Open
MatheusMartinho wants to merge 2 commits into
evolution-foundation:developfrom
MatheusMartinho:fix/issue-2678-mimetype-guard
Open

fix(meta): guard non-string mimetype before image check (fixes #2678)#2691
MatheusMartinho wants to merge 2 commits into
evolution-foundation:developfrom
MatheusMartinho:fix/issue-2678-mimetype-guard

Conversation

@MatheusMartinho

@MatheusMartinho MatheusMartinho commented Aug 13, 2026

Copy link
Copy Markdown

📋 Description

mimeTypes.lookup() returns false, not undefined, 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 in processAudio() returns false and prepareMedia.mimetype is set to false.

Line 1128 of whatsapp.business.service.ts then runs:

const isImage = message['mimetype']?.startsWith('image/');

Optional chaining only short-circuits on null and undefined. false is neither, so the call reaches false.startsWith and throws TypeError: mimetype?.startsWith is not a function before the request ever leaves for Meta.

This changes the check to an explicit type guard:

const isImage = typeof message['mimetype'] === 'string' && message['mimetype'].startsWith('image/');

🔗 Related Issue

Closes #2678

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced
  • Tested with different connection types (if applicable)

I reproduced the failure without a Chatwoot instance, by running the repository's own mime-types dependency against the exact expression from line 1128:

STEP 1 - what mimeTypes.lookup() returns
   false         Chatwoot (ActiveStorage, no extension)
   false         Chatwoot (bare blob)
   "audio/ogg"   normal URL with extension

STEP 2 - the object processAudio() returns
   {"mediaType":"audio","type":"link","mimetype":false}
   typeof message.mimetype: boolean

STEP 3 - line 1128 on develop, before the fix
   THROWS -> TypeError: message.mimetype?.startsWith is not a function

With the guard in place all four cases behave, and the existing behaviour for real MIME strings is unchanged:

mimetype isImage
false (Chatwoot) false
undefined false
"audio/ogg" false
"image/png" true

Gates run locally on this branch (based on the current develop tip):

  • npm run lint:check — clean
  • npm run build (tsc --noEmit + tsup) — passes

I did not run npm test: the script points at ./test/all.test.ts, which does not exist on develop.

📸 Screenshots (if applicable)

N/A — backend-only change.

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

Why only audio is affected. In chatwoot.service.ts, sendAttachment() downloads the file specifically to read its Content-Type when the extension-based lookup fails:

let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';

if (!mimeType) {
  const response = await axios.get(media, { responseType: 'arraybuffer' });
  mimeType = response.headers['content-type'] as string;
}

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 gets false.

This looks like an oversight rather than intent. The line above, in chatwoot.service.ts, already normalizes the same call with || ''. The false return value is understood elsewhere in the codebase, it is just not handled at this consumer. processAudio() and processMedia() even declare let 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 in src/, this one. processMedia() (lines 1379 and 1383 on develop) uses the same string | false pattern 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. Normalizing false to undefined in processAudio() alone would fix audio only.

No behaviour change on the audio path. For audio, isImage is only read inside a spread that already excludes audio, 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() and processMedia() 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.

…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.
@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

This PR hardens the WhatsApp Business integration by ensuring mime type checks only run on string values, preventing crashes when mime-type resolution returns false for extension-less URLs (e.g., Chatwoot ActiveStorage audio attachments).

Sequence diagram for guarded mimetype image check in WhatsApp Business service

sequenceDiagram
  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
Loading

File-Level Changes

Change Details Files
Guard mime-type image check so it only executes when the mimetype value is a string, avoiding false.startsWith runtime errors.
  • Replace optional-chaining based image detection with a combined typeof check plus string prefix check for the mimetype value.
  • Add an explanatory comment documenting mimeTypes.lookup() returning false for unresolved types and why optional chaining is insufficient.
  • Keep existing message payload construction logic intact so audio and other non-image media behave as before while removing the crash.
src/api/integrations/channel/meta/whatsapp.business.service.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • 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.
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.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@MatheusMartinho

Copy link
Copy Markdown
Author

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 startsWith('image/') has exactly one call site in src/, this one, so there is a single place to guard today.

I did consider normalizing at the producer. The reason I did not is that processAudio() and processMedia() both use the string | false pattern and both feed this same line, so fixing only processAudio() would leave processMedia() exposed, and fixing both means touching two producers to protect one consumer. I also leaned away from mapping false to '' because an empty string asserts a known-empty type, while the real state is "unknown".

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 processAudio() and processMedia() and drop the guard here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant