Skip to main content

Overview

Paper Channel uses pn-safe-storage to store and retrieve all document attachments associated with paper notifications — notification documents, F24 tax payment forms, and Avvisi di Avvenuta Ricezione (AAR). Safe Storage is a content-addressed, legally archiving blob store: every file is addressed by a fileKey, and files can be hot (immediately downloadable) or cold (requires a warm-up period).

Download

SafeStorageClient.getFile(fileKey) retrieves file metadata and a presigned download URL.

Upload

SafeStorageClient.createFile(...) reserves a presigned upload URL, then Paper Channel PUTs the content directly.

When Paper Channel calls Safe Storage

  • Prepare phase — attachment metadata (SHA-256, page count, document type) is fetched via getFile to validate each attachment before the send request is built.
  • ZIP / legal-facts upload — when Paper Channel generates a replica of external legal facts (PN_EXTERNAL_LEGAL_FACTS_REPLICA), it creates a new file in Safe Storage via createAndUploadContent.
  • Recursive retry on cold files — if Safe Storage returns a retryAfter hint, SafeStorageServiceImpl.getFileRecursive retries after the specified delay.

Clients

Paper Channel uses two separate generated clients:
ClientPurposeAPI class
FileDownloadApi (synchronous)Fetch file metadata and download URLpnsafestorage generated client
FileUploadApi (reactive WebClient)Create a presigned upload slotsafestorage_reactive generated client
Both are injected into SafeStorageClientImpl.

Operations

getFile

// SafeStorageClientImpl.java
public Mono<FileDownloadResponseDto> getFile(String fileKey) {
    return fileDownloadApi.getFile(fileKey, this.pnPaperChannelConfig.getSafeStorageCxId(), false)
            .retryWhen(
                Retry.backoff(2, Duration.ofMillis(500))
                    .filter(t -> t instanceof TimeoutException || t instanceof ConnectException)
            )
            .map(response -> {
                if (response.getDownload() != null && response.getDownload().getRetryAfter() != null) {
                    throw new PnRetryStorageException(response.getDownload().getRetryAfter());
                }
                response.setKey(reqFileKey);
                return response;
            });
}
  • Calls GET /safe-storage/v1/files/{fileKey}
  • Automatically strips any safestorage:// prefix from the key via AttachmentsConfigUtils.cleanFileKey
  • Retries up to 2 times with 500 ms backoff on TimeoutException or ConnectException
  • If the response contains a retryAfter field, throws PnRetryStorageException to signal the file is cold

createFile

// SafeStorageClientImpl.java
public Mono<FileCreationResponse> createFile(
    FileCreationWithContentRequest fileCreationRequestWithContent,
    String sha256
) {
    FileCreationRequest req = new FileCreationRequest();
    req.setContentType(fileCreationRequestWithContent.getContentType());
    req.setDocumentType(fileCreationRequestWithContent.getDocumentType());
    req.setStatus(fileCreationRequestWithContent.getStatus());
    return fileUploadApi.createFile(
        pnPaperChannelConfig.getSafeStorageCxId(),
        "SHA-256",
        sha256,
        req
    );
}
  • Calls POST /safe-storage/v1/files
  • Returns a FileCreationResponse containing a presigned upload URL and the new fileKey

Service layer — SafeStorageService

SafeStorageServiceImpl wraps the low-level client and adds higher-level workflows:
MethodDescription
getFileRecursive(n, fileKey, millis)Retries getFile up to n times, waiting millis milliseconds between attempts. Terminates with DOCUMENT_URL_NOT_FOUND if n drops below zero.
downloadFileAsByteArray(url)Downloads the actual file bytes from the presigned URL via HttpConnector.
downloadFile(url)Downloads and parses the file as a PDDocument (Apache PDFBox).
createAndUploadContent(request)Computes SHA-256, calls createFile, then PUTs the content to the presigned URL, and returns the new fileKey.

PnAttachmentInfo entity

Attachment metadata is persisted in DynamoDB using PnAttachmentInfo:
FieldDescription
idUnique identifier
documentTypeDocument type (e.g. PN_AAR, attachment document type)
fileKeySafe Storage file key (safestorage://...)
urlPresigned download URL (populated after getFile)
checksumSHA-256 checksum
numberOfPagePage count
dateTimestamp
filterResultCodeResult code from the document filter rule engine
filterResultDiagnosticHuman-readable description of the filter result

File lifecycle constants

// SafeStorageService.java
String SAFESTORAGE_PREFIX   = "safestorage://";          // prefix on stored fileKey values
String ZIP_HANDLE_DOC_TYPE  = "PN_EXTERNAL_LEGAL_FACTS_REPLICA"; // document type for zip replicas
String SAVED_STATUS         = "SAVED";                   // status set when creating a file

Configuration

PropertyDescriptionExample
pn.paper-channel.client-safe-storage-basepathBase URL of the Safe Storage servicehttp://localhost:8120
pn.paper-channel.safe-storage-cx-idClient identity (x-pagopa-safestorage-cx-id)pn-cons-000
pn.paper-channel.attempt-safe-storageMax retries when fetching a file synchronously3
pn.paper-channel.attempt-queue-safe-storageMax SQS consumer retries for Safe Storage events-1 (unlimited)

Error handling

ExceptionCauseBehaviour
PnRetryStorageExceptionFile is cold — retryAfter present in responsegetFileRecursive catches this and schedules a delayed retry
DOCUMENT_URL_NOT_FOUNDgetFileRecursive exhausted all retriesFatal error propagated upstream
DOCUMENT_NOT_DOWNLOADEDHTTP download of the presigned URL failedFatal error propagated upstream
WebClientResponseExceptionNon-2xx response from Safe StorageError body is logged; exception is re-emitted
ERROR_CODE_PAPERCHANNEL_ZIP_HANDLEFailure during SHA-256 computation or file creationWrapped in PnGenericException and propagated
If attempt-queue-safe-storage is set to -1, the SQS consumer will retry indefinitely on Safe Storage events. Set a positive value in production to avoid infinite loops on persistent failures.

Build docs developers (and LLMs) love