Skip to content

fix: prevent media processing failures from aborting webhook delivery#266

Open
dumilani wants to merge 1 commit intoasternic:mainfrom
dumilani:fix/prevent-webhook-abort-on-media-failure
Open

fix: prevent media processing failures from aborting webhook delivery#266
dumilani wants to merge 1 commit intoasternic:mainfrom
dumilani:fix/prevent-webhook-abort-on-media-failure

Conversation

@dumilani
Copy link

Problem

When skipMedia is false (the default), the events.Message handler in wmiau.go attempts to download and process media (image, audio, document, video, sticker) before sending the webhook to the consumer.

Each media processing block contains multiple return statements that abort the entire event handler if any step fails — directory creation, media download, file write, or base64 conversion. When this happens, the webhook is never sent, and the message silently disappears from the consumer's perspective.

Example (image block, same pattern in all 5 media types):

img := evt.Message.GetImageMessage()
if img != nil {
    // ...
    data, err := mycli.WAClient.Download(context.Background(), img)
    if err != nil {
        log.Error().Err(err).Msg("Failed to download image")
        return  // ← Aborts entire handler, webhook never sent!
    }
    // ...
}

Impact

  • Messages with media (images, audio, video, documents, stickers) are silently dropped whenever media processing fails
  • The consumer has no way to know the message existed
  • This is especially problematic because many consumers download media independently via the /chat/downloadimage, /chat/downloadvideo, /chat/downloaddocument, and /chat/downloadaudio API endpoints using the encryption keys from the message payload — they don't need the pre-processed media from the webhook at all
  • Transient failures (network issues, disk space, permission errors) cause permanent message loss

Affected media types

All five: image, audio, document, video, sticker.

Solution

Wrap each media processing block's body in an anonymous function (closure), so that return statements only exit the closure — not the parent event handler:

img := evt.Message.GetImageMessage()
if img != nil {
    func() {
        // ...
        data, err := mycli.WAClient.Download(context.Background(), img)
        if err != nil {
            log.Error().Err(err).Msg("Failed to download image")
            return  // ← Now only exits the closure, handler continues
        }
        // ...
    }()
}

This ensures the webhook is always delivered to the consumer, regardless of whether media processing succeeded or failed. Media processing errors are still logged, and the --skipmedia flag continues to work as before.

Changes

  • wmiau.go: Wrapped the body of each media processing block (image, audio, document, video, sticker) in func() { ... }() closures

Testing

  • Verified that messages with media are delivered via webhook even when media download fails
  • Verified that --skipmedia flag behavior is unchanged
  • Verified that successful media processing still populates postmap fields correctly (base64, s3, etc.)

When skipMedia is false, each media processing block (image, audio,
document, video, sticker) contains multiple return statements that
abort the entire event handler if any step fails (e.g., directory
creation, media download, file write, base64 conversion).

This means that if media processing fails for any reason, the webhook
is never sent to the consumer, causing the message to silently
disappear. The consumer never knows the message existed.

This is a critical issue because many consumers handle media downloads
independently via the /chat/downloadimage (and similar) API endpoints
using the encryption keys provided in the message payload. They do not
depend on the pre-processed media from the webhook, so aborting the
webhook is unnecessary and harmful.

The fix wraps each media processing block in an anonymous function
(closure) so that return statements only exit the closure, not the
parent event handler. This ensures the webhook is always delivered
regardless of media processing outcome.

Affected media types: image, audio, document, video, sticker.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @dumilani, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves a critical issue where transient failures during media processing could lead to silent message loss by prematurely aborting webhook delivery. The changes ensure that all webhooks are consistently delivered to consumers, irrespective of media processing outcomes, significantly enhancing message reliability and preventing data loss.

Highlights

  • Prevented webhook delivery abortion: Media processing failures (e.g., directory creation, download, file write, base64 conversion) previously caused the entire event handler to abort, leading to silent message loss and preventing webhooks from being sent to consumers.
  • Ensured reliable webhook delivery: By wrapping each media processing block (image, audio, document, video, sticker) in an anonymous function (closure), return statements now only exit the closure, allowing the main event handler to continue execution and deliver the webhook regardless of media processing success or failure.
  • Maintained error visibility: Media processing errors are still logged, providing necessary visibility into failures without blocking the critical path of webhook delivery.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • wmiau.go
    • Wrapped image message processing logic in an anonymous function to isolate its return statements.
    • Wrapped audio message processing logic in an anonymous function to isolate its return statements.
    • Wrapped document message processing logic in an anonymous function to isolate its return statements.
    • Wrapped video message processing logic in an anonymous function to isolate its return statements.
    • Wrapped sticker message processing logic in an anonymous function to isolate its return statements.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request correctly addresses the issue where media processing failures would abort the entire event handler, preventing webhook delivery. By wrapping each media processing block in an anonymous closure, return statements now only exit the local scope, allowing the handler to proceed and send the webhook. This is a significant improvement for reliability. However, the implementation highlights some existing vulnerabilities within these blocks, such as potential panics due to unchecked slice access and temporary file leaks when errors occur. Addressing these would further enhance the robustness of the media handling logic.

// try to get Image if any
img := evt.Message.GetImageMessage()
if img != nil {
func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

high

While wrapping this block in a closure prevents the main event handler from aborting on error, the implementation inside (specifically line 860) is susceptible to a panic if mime.ExtensionsByType returns an empty slice (which happens for unknown or empty MIME types). Additionally, any early return within this closure (e.g., at line 901) will bypass the os.Remove(tmpPath) call at line 914, leading to temporary file leaks in /tmp. It is recommended to use defer os.Remove(tmpPath) once the path is determined to ensure cleanup regardless of how the closure exits.

// try to get Video if any
video := evt.Message.GetVideoMessage()
if video != nil {
func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This block is also susceptible to a panic at line 1129 if exts is empty. Furthermore, early returns will bypass the os.Remove call at line 1183, causing file leaks. Implementing a fallback extension (similar to the audio block's logic) and using defer for cleanup is advised.

// try to get Audio if any
audio := evt.Message.GetAudioMessage()
if audio != nil {
func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the image block, early returns within this closure (e.g., at line 991) will skip the file cleanup at line 1004. Using defer os.Remove(tmpPath) would prevent potential file leaks.

// try to get Document if any
document := evt.Message.GetDocumentMessage()
if document != nil {
func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Early returns within this closure (e.g., at line 1086) will skip the file cleanup at line 1099. Consider using defer for more reliable resource management.


sticker := evt.Message.GetStickerMessage()
if sticker != nil {
func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Early returns within this closure (e.g., at line 1251) will skip the file cleanup at line 1262. Using defer os.Remove(tmpPath) is recommended to ensure temporary files are always deleted.

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

Comments