Skip to content
This repository has been archived by the owner on Sep 26, 2022. It is now read-only.

(Draft) Feat: ability to abort requests #198

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion android/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 0 additions & 10 deletions android/.idea/runConfigurations.xml

This file was deleted.

4 changes: 4 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ android {
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

repositories {
Expand Down
14 changes: 14 additions & 0 deletions android/src/main/java/com/getcapacitor/plugin/http/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ public void del(final PluginCall call) {
this.http(call, "DELETE");
}

@PluginMethod
public void __abortRequest(final PluginCall call) {
try {
Integer abortCode = call.getInt("abortCode");

HttpRequestHandler.abortRequest(abortCode);

call.resolve();
} catch (Exception e) {
System.out.println(e.toString());
call.reject(e.getClass().getSimpleName(), e);
}
}

@PluginMethod
public void downloadFile(final PluginCall call) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand All @@ -30,6 +32,8 @@

public class HttpRequestHandler {

private static final HashMap<Integer, Runnable> abortMap = new HashMap<>();

/**
* An enum specifying conventional HTTP Response Types
* See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
Expand Down Expand Up @@ -361,6 +365,17 @@ private static String readStreamAsString(InputStream in) throws IOException {
* @throws JSONException thrown when the incoming JSON is malformed
*/
public static JSObject request(PluginCall call, String httpMethod) throws IOException, URISyntaxException, JSONException {
JSObject signal = call.getObject("signal");

Integer abortCode = signal.getInteger("abortCode");
Boolean aborted = signal.getBoolean("aborted", false);

// If the passed signal was already aborted, the request shouldn't be made. This ensures
// compatibility with the web fetch behaviour
if (aborted != null && aborted) {
throw new SocketException();
}

String urlString = call.getString("url", "");
JSObject headers = call.getObject("headers");
JSObject params = call.getObject("params");
Expand Down Expand Up @@ -398,7 +413,43 @@ public static JSObject request(PluginCall call, String httpMethod) throws IOExce

connection.connect();

return buildResponse(connection, responseType);
if (abortCode != null) {
Runnable aborter = new Runnable() {
@Override
public void run() {
connection.getHttpConnection().disconnect();
// Remove the aborter from memory to avoid leakage
abortMap.remove(abortCode);
}
};

abortMap.put(abortCode, aborter);
}

JSObject response = buildResponse(connection, responseType);

if (abortCode != null) {
// Remove the aborter from memory to avoid leakage
abortMap.remove(abortCode);
}

return response;
}

/**
* Aborts a request based on its abort code, which is generated on client side
* @param abortCode Abort code for identifying the proper abort function
* @throws IllegalArgumentException thrown when the abort code is invalid, e.g. when the
* request has already been aborted
*/
public static void abortRequest(Integer abortCode) throws IllegalArgumentException {
Runnable aborter = abortMap.get(abortCode);

if (aborter == null) {
throw new IllegalArgumentException("Invalid abort code provided");
}

aborter.run();
}

/**
Expand Down
3 changes: 2 additions & 1 deletion example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 14 additions & 9 deletions example/server/server.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import express from 'express'
import compression from 'compression'
import bodyParser from 'body-parser'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import multer from 'multer'
import path from 'path'
import express from 'express';
import compression from 'compression';
import bodyParser from 'body-parser';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import multer from 'multer';
import path from 'path';

// __dirname workaround for .mjs file
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const app = express();
const upload = multer({ dest: 'uploads/' })
const upload = multer({ dest: 'uploads/' });

const staticPath = path.join(__dirname, '/public');

Expand All @@ -35,7 +35,7 @@ app.get('/get', (req, res) => {
res.send();
});

app.get('/get-gzip', compression({ filter: (req, res) => true, threshold: 1,}), (req, res) => {
app.get('/get-gzip', compression({ filter: (req, res) => true, threshold: 1 }), (req, res) => {
const headers = req.headers;
const params = req.query;
console.log('Got headers', headers);
Expand Down Expand Up @@ -69,6 +69,11 @@ app.get('/head', (req, res) => {
res.send();
});

app.get('/abortable', (req, res) => {
res.status(200);
setTimeout(() => res.send(''), 2000);
});

app.delete('/delete', (req, res) => {
const headers = req.headers;
console.log('DELETE');
Expand Down
51 changes: 51 additions & 0 deletions example/src/components/app-home/app-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class AppHome {

@State() output: string = '';

@State() abortController: AbortController = null;

loading: HTMLIonLoadingElement;

async get(path = '/get', method = 'GET') {
Expand Down Expand Up @@ -43,8 +45,43 @@ export class AppHome {
}
}

async abortable() {
this.output = 'Requesting... This can be aborted';

this.abortController = new AbortController();

// This request shouldn't show the loading modal, since it blocks the
// user from clicking the "abort" button, which defeats the purpouse of
// this demo

try {
const ret = await Http.request({
method: 'GET',
url: this.apiUrl('/abortable'),
headers: {
'X-Fake-Header': 'Max was here',
},
params: {
size: ['XL', 'L', 'M', 'S', 'XS'],
music: 'cool',
},
signal: this.abortController.signal,
});
console.log('Got ret', ret);
this.output = JSON.stringify(ret, null, 2);
} catch (e) {
this.output = `Error: ${e.message}, ${e.platformMessage}`;
console.error(e);
} finally {
this.abortController = null;
}
}

getDefault = () => this.get();

getAbortable = () => this.abortable();
abort = () => this.abortController.abort();

getGzip = () => this.get('/get-gzip');
getJson = () => this.get('/get-json');
getHtml = () => this.get('/get-html');
Expand Down Expand Up @@ -233,6 +270,18 @@ export class AppHome {
};

render() {
const getAbortButton = () => {
if (this.abortController) {
return (
<ion-button color="danger" onClick={this.abort}>
Abort
</ion-button>
);
}

return <ion-button onClick={this.getAbortable}>Get Abortable</ion-button>;
};

return [
<ion-header>
<ion-toolbar color="primary">
Expand Down Expand Up @@ -265,6 +314,8 @@ export class AppHome {
<ion-button onClick={this.uploadFile}>Upload File</ion-button>
<ion-button onClick={this.downloadFile}>Download File</ion-button>

{getAbortButton()}

<h4>Output</h4>
<pre id="output">{this.output}</pre>
</ion-content>,
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ export interface HttpOptions {
* (already encoded, azure/firebase testing, etc.). The default is _true_.
*/
shouldEncodeUrlParams?: boolean;
/**
* This is used to bind an AbortSignal to the request being made so it can be
* aborted by the AbortController
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/AbortController
*/
signal?: AbortSignal;
}

export interface HttpParams {
Expand Down
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { registerPlugin } from '@capacitor/core';
import { Capacitor, registerPlugin } from '@capacitor/core';
import { nativeWrap } from './native';
import type { HttpPlugin } from './definitions';

const Http = registerPlugin<HttpPlugin>('Http', {
let Http = registerPlugin<HttpPlugin>('Http', {
web: () => import('./web').then(m => new m.HttpWeb()),
electron: () => import('./web').then(m => new m.HttpWeb()),
});

if (Capacitor.isNativePlatform()) {
Http = nativeWrap(Http);
}

export * from './definitions';
export { Http };
Loading