Skip to content

Issue: AWS Textract Service - 400 and CORS errors #6008

@Pratham-commits-code

Description

@Pratham-commits-code

Description

I'll help you craft a GitHub issue for the CORS and 404 errors you're experiencing. Here's a template you can use:

## Issue: AWS Textract Service - 404 in OPTIONS request and CORS errors

### Description
I'm experiencing CORS errors when trying to use the AWS Textract service from a Flutter application. The browser is sending an OPTIONS preflight request which is resulting in a 404 error, followed by CORS errors that prevent the actual Textract API call from succeeding.

### Environment
- Flutter version: Flutter 3.29.0
- AWS SDK packages:
  - aws_common: 
  - aws_signature_v4: 
- Browser : [Chrome.]
- Platform: [Web]

### Reproduction Steps
1. Initialize the AwsTextractService with valid AWS credentials
2. Call the `analyzeDocumentClarity` method with document bytes
3. The browser sends an OPTIONS preflight request to the Textract endpoint
4. The OPTIONS request receives a 404 response
5. Subsequent POST request fails with CORS error

### Code Example
```dart
final service = AwsTextractService.getInstance(
  accessKey: 'MY_ACCESS_KEY',
  secretKey: 'MY_SECRET_KEY',
  region: 'ap-south-1',
);

try {
  final result = await service.analyzeDocumentClarity(documentBytes);
  // Never gets here due to CORS error
} catch (e) {
  print('Error: $e');
}

Error Messages

Error analyzing document: POST https://textract.ap-south-1.amazonaws.com? failed: TypeError: Failed to fetch
DartError: Bad state: Future already completed

Expected Behavior

The AWS Textract API call should complete successfully without CORS errors.

Attempted Solutions

{"__type":"InvalidSignatureException","message":"The request signature we calculated does not match the signature you
provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
  • I've also tried to compare the client sdk for react and how it does things but couldn't replicate it.
  • I've verified the credentials
  • I've confirmed direct API calls (e.g., from Postman) work correctly.

Would appreciate guidance on:

  1. Is there a different approach needed for browser-based Textract calls?
  2. Are there additional headers needed for CORS support?

What I am trying to do.

import 'dart:convert';
import 'dart:typed_data';
import 'package:aws_textract_api/textract-2018-06-27.dart';
import 'package:aws_common/aws_common.dart';
import 'package:aws_signature_v4/aws_signature_v4.dart';
import 'package:crypto/crypto.dart';
import 'package:esc_pos_utils_plus/dart_hex/hex.dart';
import 'package:flutter/foundation.dart';
import 'package:uuid/uuid.dart';

/// AWS Textract service that uses the aws_signature_v4 package for authentication
class AwsTextractService {
  static AwsTextractService? _instance;

  final String accessKey;
  final String secretKey;
  final String region;
  final AWSCredentials _credentials;
  final AWSCredentialsProvider _credentialsProvider;
  late final AWSSigV4Signer _signer;
  final Uuid _uuid = const Uuid();

  // Private constructor for singleton
  AwsTextractService._({
    required this.accessKey,
    required this.secretKey,
    required this.region,
  })  : _credentials = AWSCredentials(accessKey, secretKey),
        _credentialsProvider =
            AWSCredentialsProvider(AWSCredentials(accessKey, secretKey)) {
    _signer = AWSSigV4Signer(
      credentialsProvider: _credentialsProvider,
    );
  }

  /// Get the singleton instance
  static AwsTextractService getInstance({
    required String accessKey,
    required String secretKey,
    String region = 'ap-south-1',
  }) {
    _instance ??= AwsTextractService._(
      accessKey: accessKey,
      secretKey: secretKey,
      region: region,
    );
    return _instance!;
  }

  String _formatDate(DateTime dateTime) {
    return '${dateTime.year}${dateTime.month.toString().padLeft(2, '0')}${dateTime.day.toString().padLeft(2, '0')}';
  }

  String _formatAmzDate(DateTime dateTime) {
    return '${_formatDate(dateTime)}T${dateTime.hour.toString().padLeft(2, '0')}${dateTime.minute.toString().padLeft(2, '0')}${dateTime.second.toString().padLeft(2, '0')}Z';
  }

  /// Analyze document clarity using AWS Textract
  /// Returns a confidence score between 0.0 and 1.0
  Future<double> analyzeDocumentClarity(Uint8List documentBytes) async {
    try {
      // Host and endpoint information
      final host = 'textract.$region.amazonaws.com';
      final endpoint = Uri.parse('https://$host');
      final timestamp = DateTime.now().toUtc();
      final amzDate = _formatAmzDate(timestamp);

      // Prepare the request body
      final requestBody = jsonEncode({
        'Document': {
          'Bytes': base64Encode(documentBytes),
        },
      });

      // Create a unique request ID
      final requestId = _uuid.v4();

      // Convert request body to bytes
      final requestBodyBytes = Uint8List.fromList(utf8.encode(requestBody));

      // Calculate SHA256 hash of request body
      final bodySha256 = sha256.convert(requestBodyBytes).bytes;
      final hexEncodedBodyHash = const HexEncoder().convert(bodySha256);

      // Prepare headers
      final headers = {
        'Content-Type': 'application/x-amz-json-1.1',
        'X-Amz-Target': 'Textract.DetectDocumentText',
        'amz-sdk-invocation-id': requestId,
        'amz-sdk-request': 'attempt=1; max=3',
        'Accept': '*/*',
        'X-Amz-Date': amzDate,
        'X-Amz-Content-Sha256': hexEncodedBodyHash,
      };

      debugPrint('Headers: $headers');

      // Create the AWS request
      final awsRequest = AWSHttpRequest(
        method: AWSHttpMethod.post,
        uri: endpoint,
        headers: headers,
        body: requestBodyBytes,
      );

      // Define the credential scope
      final scope = AWSCredentialScope(
        region: region,
        service: AWSService.textract,
      );

      debugPrint('Request: $awsRequest');

      // Sign the request
      final AWSSignedRequest signedRequest = await _signer.sign(
        awsRequest,
        credentialScope: scope,
        serviceConfiguration: S3ServiceConfiguration(), // Using S3 config as fallback
      );

      debugPrint('Signed Headers: ${signedRequest.headers}');

      // Send the request using AWS HTTP client
      final operation = signedRequest.send();

      // Handle the response
      final response = await operation.response;

      if (response.statusCode != 200) {
        final errorBody = await _decodeResponse(response);
        throw Exception(
          'AWS Textract request failed with status ${response.statusCode}: $errorBody',
        );
      }

      // Parse the response
      final responseBody = await _decodeResponse(response);
      final responseJson = jsonDecode(responseBody);
      final textractResponse = DetectDocumentTextResponse.fromJson(responseJson);

      // Calculate and return the document clarity score
      return _calculateDocumentClarityScore(textractResponse);
    } catch (e) {
      debugPrint('Error analyzing document: $e');
      rethrow;
    }
  }

  /// Helper method to decode the AWS HTTP response
  Future<String> _decodeResponse(AWSBaseHttpResponse response) async {
    return await utf8.decodeStream(response.split());
  }

  /// Calculate document clarity score based on Textract response
  double _calculateDocumentClarityScore(DetectDocumentTextResponse response) {
    final blocks = response.blocks;

    if (blocks == null || blocks.isEmpty) {
      return 0.0; // No text detected
    }

    // Calculate average confidence across all detected text blocks
    double totalConfidence = 0.0;
    int textBlockCount = 0;

    for (final block in blocks) {
      if (block.blockType == BlockType.line ||
          block.blockType == BlockType.word) {
        if (block.confidence != null) {
          totalConfidence += block.confidence!;
          textBlockCount++;
        }
      }
    }

    // If no text blocks found, document might be blank or an image
    if (textBlockCount == 0) {
      return 0.3; // Assign a low default score
    }

    // Return average confidence as a value between 0.0 and 1.0
    return totalConfidence / textBlockCount / 100;
  }
}

Categories

  • API (REST)
  • Analytics
  • API (GraphQL)
  • Auth
  • Authenticator
  • DataStore
  • Notifications (Push)
  • Storage

Steps to Reproduce

Here's a detailed "Steps to Reproduce" section you can add to your GitHub issue:

### Steps to Reproduce

1. Create a new Flutter web project or use an existing one
2. Add the following dependencies to your pubspec.yaml:
   ```yaml
   dependencies:
    aws_signature_v4: ^0.6.3
    aws_common: ^0.7.5
    uuid: ^4.5.1
    crypto: ^3.0.6
    dotenv: ^4.2.0
    esc_pos_utils_plus: ^2.0.4
  1. Create a service class for AWS Textract (as shown in code example)

  2. Set up a simple UI with:

    • A button to select/capture a document image
    • A button to analyze the document using Textract
    • A text area to display results or errors
  3. Implement the document analysis workflow:

    // In your widget's event handler
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.gallery);
    
    if (image != null) {
      final bytes = await image.readAsBytes();
      
      try {
        final service = AwsTextractService.getInstance(
          accessKey: 'YOUR_ACCESS_KEY',
          secretKey: 'YOUR_SECRET_KEY',
          region: 'ap-south-1',
        );
        
        final clarity = await service.analyzeDocumentClarity(bytes);
        setState(() {
          resultText = "Document clarity score: ${clarity.toStringAsFixed(2)}";
        });
      } catch (e) {
        setState(() {
          resultText = "Error: $e";
        });
        print('Full error: $e');
      }
    }
  4. Select a document image and click the analyze button

  5. Observe in the Network tab:

    • An OPTIONS request to the Textract endpoint failing with a 404 status
    • The subsequent POST request failing with a CORS error
  6. Check the Console tab for the complete error message about CORS policy violation



### Screenshots

<img width="1088" alt="Image" src="https://github.com/user-attachments/assets/e1738002-fa29-4dbf-902d-8c17dc0dbb9b" />

<img width="1081" alt="Image" src="https://github.com/user-attachments/assets/10aa6a7c-098a-4370-8eb4-9d359fc09663" />

### Platforms

- [ ] iOS
- [ ] Android
- [x] Web
- [ ] macOS
- [ ] Windows
- [ ] Linux

### Flutter Version

3.29.0

### Amplify Flutter Version

aws_common: ^0.7.5

### Deployment Method

Amplify Gen 2

### Schema

```GraphQL

Metadata

Metadata

Assignees

No one assigned

    Labels

    authIssues related to the Auth CategorybugSomething is not working; the issue has reproducible steps and has been reproducedcoreIssues related to the Amplify Core PluginsigV4SignerIssues specific to the SigV4Signer package

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions