Skip to content

fix: iOS image share intent uploads actual images instead of file paths#113

Open
Melvynx wants to merge 2 commits intomainfrom
fix-ios-image-share
Open

fix: iOS image share intent uploads actual images instead of file paths#113
Melvynx wants to merge 2 commits intomainfrom
fix-ios-image-share

Conversation

@Melvynx
Copy link
Copy Markdown
Owner

@Melvynx Melvynx commented Sep 22, 2025

Summary

Fixes iOS share intent to upload actual image blobs instead of treating local file paths as URLs.

Changes Made

  • Mobile API client: Added FormData support alongside existing JSON requests
  • Share handler: Enhanced to detect images and read file data from local paths
  • API endpoint: Modified /api/v1/bookmarks to accept image files with proper validation
  • S3 integration: Uses existing upload patterns (2MB limit, MIME type validation)
  • Processing: Uploaded images trigger existing Inngest AI analysis pipeline

Technical Details

  • Follows existing avatar upload patterns for consistency
  • No new dependencies or breaking changes
  • Maintains backward compatibility for non-image shares
  • Uses established error handling and validation

Test Plan

  • TypeScript compilation passes
  • ESLint validation passes
  • Follows existing code patterns
  • Uses established S3 upload utilities
  • Integrates with existing Inngest processing

Before/After

Before: iOS shared images failed - local file paths sent as URLs
After: iOS shared images upload properly and get AI processing

Closes #111

… paths

- Updated mobile API client to support FormData uploads alongside JSON
- Modified share handler to detect images and read file data from local path
- Enhanced /api/v1/bookmarks endpoint to accept image files with validation
- Added S3 upload integration following existing avatar upload patterns
- Images now trigger proper Inngest processing pipeline for AI analysis

Closes #111
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Sep 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
saveit-now-web Ready Ready Preview Comment Sep 22, 2025 2:54am

- Use /api/bookmarks instead of /api/v1/bookmarks for authenticated users
- Enhanced main bookmarks API to support both JSON and FormData requests
- Fixed TypeScript typing for metadata parameter
- Mobile app now sends images via FormData to correct endpoint
- Maintains backward compatibility for existing bookmark creation

Addresses feedback on PR #113
@Melvynx
Copy link
Copy Markdown
Owner Author

Melvynx commented Oct 8, 2025

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting

Comment on lines +19 to +83
export const POST = userRoute.body(z.any()).handler(async (req, { ctx }) => {
try {
const contentType = req.headers.get("content-type") || "";
let url: string;
let transcript: string | undefined;
let metadata: unknown;

if (contentType.includes("multipart/form-data")) {
const formData = await req.formData();
url = formData.get("url") as string;
transcript = (formData.get("transcript") as string) || undefined;
const metadataString = formData.get("metadata") as string;
const imageFile = formData.get("image") as File | null;

if (!url) {
return NextResponse.json({ error: "URL is required" }, { status: 400 });
}

if (metadataString) {
try {
metadata = JSON.parse(metadataString);
} catch {
metadata = {};
}
}

if (imageFile) {
if (imageFile.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: "File size must be less than 2MB" },
{ status: 400 },
);
}

if (!ALLOWED_IMAGE_TYPES.includes(imageFile.type)) {
return NextResponse.json(
{ error: "Only image files (JPEG, PNG, WebP, GIF) are allowed" },
{ status: 400 },
);
}

const s3Url = await uploadFileToS3({
file: imageFile,
prefix: `users/${ctx.user.id}/bookmarks`,
fileName: `${Date.now()}-${imageFile.name}`,
contentType: imageFile.type,
});

if (!s3Url) {
return NextResponse.json(
{ error: "Failed to upload image" },
{ status: 500 },
);
}

url = s3Url;
metadata = {
...(metadata as Record<string, unknown>),
originalFileName: imageFile.name,
uploadedFromMobile: true,
};
}
} else {
const body = await req.json();
url = body.url;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid double-reading request body in bookmark POST

The route is still wrapped with userRoute.body(z.any()), which causes next-zod-route to consume the request body before the handler executes. The new implementation then calls req.formData()/req.json() again, which will throw TypeError: body stream already read and prevent both JSON and multipart submissions from ever reaching createBookmark. Drop the .body() middleware and parse the request only once (or reuse the parsed body) so image uploads can succeed.

Useful? React with 👍 / 👎.

Comment on lines +19 to +85
export const POST = apiRoute.body(z.any()).handler(async (req, { ctx }) => {
try {
const formData = await req.formData();
const url = formData.get("url") as string;
const metadataString = formData.get("metadata") as string;
const imageFile = formData.get("image") as File | null;

if (!url) {
return NextResponse.json(
{ error: "URL is required", success: false },
{ status: 400 },
);
}

let metadata: Record<string, unknown> = {};
if (metadataString) {
try {
metadata = JSON.parse(metadataString);
} catch {
metadata = {};
}
}

let actualUrl = url;

return {
success: true,
bookmark: {
id: bookmark.id,
url: bookmark.url,
title: bookmark.title,
summary: bookmark.summary,
type: bookmark.type,
status: bookmark.status,
starred: bookmark.starred,
read: bookmark.read,
createdAt: bookmark.createdAt,
updatedAt: bookmark.updatedAt,
},
};
} catch (error: unknown) {
if (error instanceof BookmarkValidationError) {
if (imageFile) {
if (imageFile.size > MAX_FILE_SIZE) {
return NextResponse.json(
{ error: error.message, success: false },
{ error: "File size must be less than 2MB", success: false },
{ status: 400 },
);
}

throw error;
if (!ALLOWED_IMAGE_TYPES.includes(imageFile.type)) {
return NextResponse.json(
{
error: "Only image files (JPEG, PNG, WebP, GIF) are allowed",
success: false,
},
{ status: 400 },
);
}

const s3Url = await uploadFileToS3({
file: imageFile,
prefix: `users/${ctx.user.id}/bookmarks`,
fileName: `${Date.now()}-${imageFile.name}`,
contentType: imageFile.type,
});

if (!s3Url) {
return NextResponse.json(
{ error: "Failed to upload image", success: false },
{ status: 500 },
);
}

actualUrl = s3Url;
metadata.originalFileName = imageFile.name;
metadata.uploadedFromMobile = true;
}
});

const bookmark = await createBookmark({
url: actualUrl,
userId: ctx.user.id,
metadata,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep JSON and transcript support in v1 bookmark POST

The v1 endpoint now unconditionally calls req.formData() and never forwards a transcript to createBookmark. Existing clients that still POST JSON (as the previous version required) will fail when formData() is invoked on an application/json request, and any transcripts sent by those clients will be discarded. To maintain backward compatibility, detect content-type and handle JSON bodies while preserving the transcript field.

Useful? React with 👍 / 👎.

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.

Share of image with iOS app doesn't work

1 participant